git-cpan-module: SOAP-WSDL git-cpan-version: 1.18 git-cpan-authorid: MKUTTER git-cpan-file: authors/id/M/MK/MKUTTER/SOAP-WSDL-1.18.tar.gz
1059 lines
35 KiB
Perl
1059 lines
35 KiB
Perl
#!/usr/bin/perl -w
|
|
package SOAP::WSDL;
|
|
|
|
use SOAP::Lite;
|
|
use vars qw($VERSION @ISA);
|
|
use XML::XPath;
|
|
use Cache::FileCache;
|
|
|
|
use Data::Dumper;
|
|
# use Carp;
|
|
use diagnostics;
|
|
|
|
@ISA= qw(SOAP::Lite);
|
|
|
|
# let CVS handle this for you...
|
|
$VERSION = sprintf("%d.%02d", q$Revision: 1.18 $ =~ /(\d+)\.(\d+)/);
|
|
|
|
sub wsdlinit {
|
|
my $self=shift;
|
|
my %opt=@_;
|
|
$self->{_WSDL}->{ cache } = {};
|
|
$self->{_WSDL}->{ caching } = $opt{ caching };
|
|
$self->{_WSDL}->{ cache_directory } = $opt{ cache_directory } if exists($opt{cache_directory});
|
|
$self->{_WSDL}->{ checkoccurs } = 1 unless defined( $self->{_WSDL}->{ checkoccurs } );
|
|
|
|
if (($self->{_WSDL}->{ caching }) && (! $self->{_WSDL }->{ fileCache })) {
|
|
$self->wsdl_cache_init()
|
|
};
|
|
|
|
my $location=$self->transport->proxy->endpoint
|
|
|| die "Error processing WSDL: No port specified. ".
|
|
"You need to call port() before calling wsdlinit";
|
|
|
|
# makeup xpath document
|
|
my $xpath;
|
|
# check cache
|
|
if ($self->{_WSDL}->{ fileCache }) {
|
|
# get xpath from cache
|
|
$xpath = $self->{_WSDL}->{ fileCache }->get( $self->wsdl );
|
|
# get in-memory cache from cache
|
|
if ($self->{_WSDL}->{ caching }) {
|
|
my $cache=$self->{_WSDL}->{ fileCache }->get( $self->wsdl."_cache" );
|
|
$self->{_WSDL}->{ cache } = $cache || {};
|
|
}
|
|
}
|
|
unless ($xpath) {
|
|
$xpath = XML::XPath->new(
|
|
xml => SOAP::Schema->new(schema => $self->wsdl)->access );
|
|
}
|
|
|
|
($xpath) ||
|
|
die "Error processing WSDL: Cannot create XPath object";
|
|
|
|
# Get root element (<definitions>) and get
|
|
# default prefix (the root element's one).
|
|
my $definitions=undef;
|
|
$definitions=$xpath->find('/*[1]')->shift;
|
|
my $prefix= $definitions->getPrefix;
|
|
$self->_wsdl_wsdlns( $prefix ? $prefix.':' : '' );
|
|
|
|
# get targetNamespace
|
|
my $tns= $definitions->getAttribute('targetNamespace')
|
|
|| die "Error processing WSDL: cannot get <definition targetNamespace=''>";
|
|
|
|
# look for schema namespace & prefix for targetNamespace
|
|
my ($defaultNS, $schemaNS) = ('','');
|
|
my @_ns_sub_list=();
|
|
|
|
my $nameSpaces=$definitions->getNamespaces
|
|
|| die "Error processing WSDL: cannot get <definition> namespaces";
|
|
my $nsHash={};
|
|
foreach my $ns( @{ $nameSpaces } ) {
|
|
$xpath->set_namespace($ns->getPrefix, $ns->getData);
|
|
if ($ns->getData eq $tns) {
|
|
push @_ns_sub_list, $ns->getPrefix;
|
|
next;
|
|
}
|
|
if ($ns->getData eq 'http://www.w3.org/2001/XMLSchema') {
|
|
$schemaNS=$ns->getPrefix;
|
|
}
|
|
$nsHash->{ $ns->getData } = $ns->getPrefix.':';
|
|
}
|
|
$self->_wsdl_ns( $nsHash );
|
|
|
|
$defaultNS=join('|', @_ns_sub_list);
|
|
$self->_wsdl_tns($defaultNS);
|
|
$self->_wsdl_tns_uri($tns);
|
|
$self->_wsdl_schemans($schemaNS);
|
|
|
|
|
|
### TBD: get portName with ONE xpath expression (yup this IS possible).
|
|
|
|
# find address in service ports
|
|
my $path=$self->servicename ? "[\@name='". $self->servicename ."']" : '';
|
|
|
|
$path="/".$self->_wsdl_wsdlns."definitions/"
|
|
.$self->_wsdl_wsdlns."service$path/"
|
|
.$self->_wsdl_wsdlns."port/"
|
|
.$self->_wsdl_ns->{'http://schemas.xmlsoap.org/wsdl/soap/'}
|
|
."address[\@location='$location']";
|
|
|
|
my $service=$xpath->find($path)
|
|
# get 1st matching node -> get parent node -> get value of "name"
|
|
->shift || die "Error processing WSDL file - no such service ($path)";
|
|
my $portName=$service->getParentNode->findvalue('@name')
|
|
|| die "Error processing WSDL: Cannot find ".
|
|
"port for location $location in service $path";
|
|
|
|
$self->_wsdl_portname($portName);
|
|
$self->_wsdl_xpath( $xpath );
|
|
}
|
|
|
|
sub call {
|
|
my $self=shift;
|
|
my $method=shift;
|
|
my %data=@_;
|
|
|
|
my $path;
|
|
|
|
my $location=$self->endpoint;
|
|
|
|
my $mode='input';
|
|
|
|
my $tns=$self->_wsdl_tns;
|
|
my $ns=$self->_wsdl_ns;
|
|
### get input|output message
|
|
|
|
my $xpath=$self->_wsdl_xpath;
|
|
|
|
($xpath) || do {
|
|
$self->wsdlinit;
|
|
$xpath=$self->_wsdl_xpath || die "Error processing WSDL: no wsdl object";
|
|
};
|
|
### jump to nodeset matching operation & take the first one (there is only one)
|
|
my $portName=$self->_wsdl_portname || die "Error processing WSDL: no port name";
|
|
|
|
$path='/' . $self->_wsdl_wsdlns . 'definitions/'
|
|
.$self->_wsdl_wsdlns . "portType[\@name='$portName']/"
|
|
.$self->_wsdl_wsdlns . "operation[\@name='$method']/"
|
|
.$self->_wsdl_wsdlns ."$mode/\@message";
|
|
|
|
# try cache first if caching is enabled
|
|
my $messageName=$self->{_WSDL}->{ caching } ? $self->{_WSDL}->{ cache }->{ $path } : undef;
|
|
|
|
unless ($messageName) {
|
|
$messageName=$xpath->findvalue($path)
|
|
|| die "Error processing WSDL: cannot find $mode message for method '$method' ($path)";
|
|
$self->{_WSDL}->{ cache }->{ $path }= $messageName if ($self->{_WSDL}->{ caching });
|
|
}
|
|
|
|
# remove default targetNamespace from messageName
|
|
$messageName=~s/^($tns)\:*//;
|
|
|
|
$path='/' . $self->_wsdl_wsdlns . 'definitions/'
|
|
. $self->_wsdl_wsdlns . "message[\@name='$messageName']/"
|
|
. $self->_wsdl_wsdlns . 'part';
|
|
|
|
# try cache first, if caching is enabled
|
|
my $parts=$self->{_WSDL}->{ caching } ? $self->{_WSDL}->{ cache }->{ $path } : undef;
|
|
unless ($parts) {
|
|
$parts= $xpath->find($path);
|
|
$self->{_WSDL}->{ cache }->{ $path }= $parts if ($self->{_WSDL}->{ caching });
|
|
}
|
|
|
|
($parts) || die "Error processing WSDL: No parts found for message $messageName with path '$path'";
|
|
|
|
my @param=();
|
|
|
|
while (my $part=$parts->shift) {
|
|
my @enc=$self->encode($part, \%data);
|
|
push @param, @enc if (@enc);
|
|
}
|
|
|
|
### TBD: encode method (fully qualified)
|
|
my $methodEncoded=SOAP::Data->name( $method )->attr( { xmlns => $self->_wsdl_tns_uri } );
|
|
|
|
unless ($self->{_WSDL}->{no_dispatch}) {
|
|
return $self->SUPER::call($methodEncoded => @param);
|
|
} else {
|
|
return $methodEncoded, @param;
|
|
}
|
|
}
|
|
|
|
sub DESTROY {
|
|
my $self=shift;
|
|
$self->wsdl_cache_store();
|
|
return 1;
|
|
}
|
|
|
|
sub no_dispatch {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{no_dispatch}=shift : $self->{_WSDL}->{no_dispatch};
|
|
}
|
|
|
|
sub wsdl {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{wsdl}=shift : $self->{_WSDL}->{wsdl};
|
|
}
|
|
|
|
sub wsdl_checkoccurs {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{ checkoccurs }=shift : $self->{_WSDL}->{ checkoccurs };
|
|
}
|
|
|
|
|
|
sub servicename {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{servicename}=shift : $self->{_WSDL}->{servicename};
|
|
}
|
|
|
|
sub wsdl_cache_directory {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{ cache_directory }=shift : $self->{_WSDL}->{ cache_directory };
|
|
}
|
|
|
|
sub wsdl_cache_store {
|
|
my $self=shift;
|
|
if ( ( $self->{ _WSDL }->{ cache_directory }) && ($self->{ _WSDL }->{ fileCache } ) ) {
|
|
$self->{ _WSDL }->{ fileCache }->set( $self->wsdl, $self->{ _WSDL }->{ xpath } );
|
|
$self->{ _WSDL }->{ fileCache }->set( $self->wsdl."_cache", $self->{ _WSDL }->{ cache } );
|
|
}
|
|
}
|
|
|
|
sub wsdl_cache_init {
|
|
my $self=shift;
|
|
my $p=shift || undef;
|
|
my $cache = undef;
|
|
if ( defined( $p ) ) {
|
|
$p->{ cache_root } = $self->{_WSDL}->{ cache_directory } unless ($p->{ cache_root });
|
|
$cache=Cache::FileCache->new( $p ) if ($p->{ cache_root });
|
|
} else {
|
|
$cache=Cache::FileCache->new( { cache_root => $self->{_WSDL}->{ cache_directory } } ) if ($self->{_WSDL}->{ cache_directory });
|
|
}
|
|
$self->{_WSDL}->{ fileCache } = $cache;
|
|
}
|
|
|
|
sub _wsdl_ns {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{namespaces}=shift : $self->{_WSDL}->{namespaces};
|
|
}
|
|
|
|
sub _wsdl_xpath {
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{xpath}=shift : $self->{_WSDL}->{xpath};
|
|
}
|
|
|
|
sub _wsdl_tns{
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{tns}=shift : $self->{_WSDL}->{tns};
|
|
}
|
|
|
|
sub _wsdl_tns_uri{
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{tns}->{uri}=shift : $self->{_WSDL}->{tns}->{uri};
|
|
}
|
|
|
|
sub _wsdl_wsdlns{
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{wsdlns}=shift : $self->{_WSDL}->{wsdlns};
|
|
}
|
|
|
|
sub _wsdl_schemans{
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{schemans}=shift : $self->{_WSDL}->{schemans};
|
|
}
|
|
|
|
sub _wsdl_portname{
|
|
my $self = shift;
|
|
return (@_) ? $self->{_WSDL}->{portname}=shift : $self->{_WSDL}->{portname};
|
|
}
|
|
|
|
sub encode {
|
|
my $self=shift;
|
|
my $part=shift;
|
|
my $data=shift;
|
|
|
|
my $schemaNS=$self->{_WSDL}->{schemans};
|
|
my $defaultNS=$self->{_WSDL}->{tns};
|
|
|
|
my %nsHash = reverse %{ $self->_wsdl_ns };
|
|
my %nsURIs = %{ $self->_wsdl_ns };
|
|
|
|
# print Dumper %nsHash;
|
|
|
|
# TBD: Caching hook ?
|
|
|
|
my $p= {
|
|
name => $part->findvalue('@name')->value,
|
|
type => $part->findvalue('@type')->value,
|
|
element => $part->findvalue('@element')->value,
|
|
xmlns => $part->findvalue('@targetNamespace')->value,
|
|
nillable => $part->findvalue('@nillable')->value,
|
|
};
|
|
|
|
my $result=undef;
|
|
my $order=undef;
|
|
|
|
if ( ($p->{type}) ) {
|
|
#
|
|
# it's either a XML schema type or an apache-soap type
|
|
my $match;
|
|
{
|
|
no warnings;
|
|
$match="$nsURIs{'http://www.w3.org/2001/XMLSchema'}|$nsURIs{'http://xml.apache.org/xml-soap'}";
|
|
$match=~s/\|$//;
|
|
};
|
|
|
|
if ( $p->{type}=~m/^$match/ )
|
|
{ ### simple type
|
|
my $count=-1;
|
|
if ($self->{_WSDL}->{checkoccurs}) {
|
|
$count= exists $data->{ $p->{ name } } ?
|
|
defined $data->{ $p->{ name } } ?
|
|
ref $data->{ $p->{ name } } eq 'ARRAY' ?
|
|
scalar @{ $data->{ $p->{ name } } }
|
|
: 1
|
|
: 0
|
|
: 0 ;
|
|
|
|
$order=$part->getParentNode()->getName;
|
|
|
|
$p->{ minOccurs }=$part->findvalue('@minOccurs')->value;
|
|
if ( (! defined($p->{ minOccurs }) ) || ( $p->{ minOccurs } eq "") ) {
|
|
if ($order eq 'sequence') { $p->{ minOccurs }=1 }
|
|
elsif ($order eq 'all') { $p->{ minOccurs }=0 }
|
|
else { $p->{ minOccurs }=0 }
|
|
}
|
|
$p->{ maxOccurs }=$part->findvalue('@maxOccurs')->value;
|
|
if ( (! defined($p->{ maxOccurs }) ) || ($p->{ maxOccurs } eq "") ) {
|
|
if ($order eq 'sequence') { $p->{ maxOccurs }=1 }
|
|
elsif ($order eq 'all') { $p->{ maxOccurs }=1 }
|
|
else { $p->{ maxOccurs }=undef }
|
|
}
|
|
$p->{ maxOccurs } = undef if (defined($p->{ maxOccurs }) && $p->{ maxOccurs } eq 'unbounded' );
|
|
}
|
|
|
|
# check for ocurrence ?
|
|
# acceptable number of value ?
|
|
if ( (! $self->{_WSDL}->{checkoccurs} )
|
|
|| ( ( $p->{ minOccurs } <=$count ) || ($p->{ nillable } eq "true") )
|
|
&& ( ( !defined($p->{ maxOccurs }) ) || ($count <= $p->{ maxOccurs }) ) ) {
|
|
# not nillable
|
|
# empty value
|
|
( ! $p->{ nillable } ) &&
|
|
( ( ! (exists $data->{ $p->{ name } }) ) || (! (defined $data->{ $p->{ name } }) ) )
|
|
&& do {
|
|
return ();
|
|
};
|
|
# some value
|
|
|
|
# SOAP::Lite uses the "xsd" prefix for specifying schema NS
|
|
my $type=$p->{ type };
|
|
$type=~s/^$schemaNS/xsd/;
|
|
$result=SOAP::Data->new( name => $p->{ name } );
|
|
$result->type( $type ) if ($self->autotype);
|
|
$result->attr( { xmlns => $p->{ xmlns } } ) if $p->{ xmlns };
|
|
return ($result->value( $data->{ $p->{ name } } ) );
|
|
} else {
|
|
no warnings;
|
|
die "illegal number of elements ($count, min: ".$p->{ minOccurs } .", max: .".$p->{ maxOccurs }.") for element '$p->{ name }' (may be sub-element) ";
|
|
}
|
|
|
|
} else { ### complex type
|
|
### get complex type
|
|
my $name=$p->{ type };
|
|
$name=~s/^$defaultNS\://;
|
|
|
|
$name=~s/^(.+?\:)?//;
|
|
my $path;
|
|
{
|
|
no warnings;
|
|
|
|
$path='/'.$self->_wsdl_wsdlns.'definitions/'
|
|
.$self->_wsdl_wsdlns."types/$schemaNS:schema/"
|
|
."$schemaNS:complexType[\@name='$name']"
|
|
.'|'
|
|
.'/'.$self->_wsdl_wsdlns.'definitions/'
|
|
.$self->_wsdl_wsdlns."types/schema[\@xmlns='". $nsHash{$schemaNS.':'} ."' and \@targetNameSpace = '". $nsHash{$1}."' ]/"
|
|
."complexType[\@name='$name']"
|
|
;
|
|
};
|
|
# got an absolute path - try cache first
|
|
my $complexType=$self->{_WSDL}->{ caching } ? $self->{_WSDL}->{ cache }->{ $path } : undef;
|
|
unless ($complexType) {
|
|
$complexType=$self->{_WSDL}->{xpath}->find($path)->shift
|
|
|| die "Error processing WSDL: $path not found";
|
|
$self->{_WSDL}->{ cache }->{ $path } = $complexType if ($self->{_WSDL}->{ caching });
|
|
}
|
|
|
|
# handle arrays of complex types
|
|
### TBD: check for min /max number of elements
|
|
if (ref $data->{ $p->{ name } } eq 'ARRAY') {
|
|
my @resultArray=();
|
|
foreach my $subdata( @{ $data->{ $p->{ name } } }) {
|
|
$result=SOAP::Data->new( name => $p->{ name } );
|
|
$result->type( $type ) if ($self->autotype);
|
|
$result->attr( { xmlns => $p->{ xmlns } } ) if $p->{ xmlns };
|
|
my $value = $self->encodeComplexType($complexType, $subdata );
|
|
push @resultArray, $result->value( $value ) if (defined($value) );
|
|
}
|
|
return (@resultArray) ? @resultArray : ();
|
|
} else {
|
|
$result=SOAP::Data->new( name => $p->{ name } );
|
|
$result->type( $type ) if ($self->autotype);
|
|
$result->attr( { xmlns => $p->{ xmlns } } ) if $p->{ xmlns };
|
|
|
|
my $value;
|
|
$value = $self->encodeComplexType($complexType, $data->{ $p->{ name } } ) ;
|
|
# || die "error encoding ". $p->{ name } .": \n\t $@";
|
|
return () unless ( defined($value) );
|
|
return ($result->value( $value));
|
|
}
|
|
}
|
|
} elsif ($p->{ element } ) {
|
|
### get element
|
|
my $elementPath=$p->{element};
|
|
$elementPath=~s/^$defaultNS\://;
|
|
|
|
my $path= '/'.$self->_wsdl_wsdlns.'definitions/'
|
|
.$self->_wsdl_wsdlns.'types/'
|
|
.$schemaNS.':schema/'
|
|
.$schemaNS.':element[@name="'.$elementPath.'"]/'
|
|
.$schemaNS.':complexType/'
|
|
.'descendant::'.$schemaNS.':element'
|
|
;
|
|
|
|
# got an absolute path - try cache first
|
|
my $elements = $self->{_WSDL}->{caching } ? $self->{_WSDL}->{cache}->{$path} : undef;
|
|
unless ($elements) {
|
|
$elements=$self->{_WSDL}->{xpath}->findnodes($path)
|
|
|| die "Error processing WSDL: '$path' not found";
|
|
$self->{_WSDL}->{cache}->{$path}= $elements if ($self->{_WSDL}->{caching});
|
|
}
|
|
|
|
my @resultArray=();
|
|
while (my $e=$elements->shift) {
|
|
my @enc;
|
|
@enc=$self->encode($e, $data);
|
|
# die "error encoding ". $p->{ name } .": \n\t$@";
|
|
push @resultArray, @enc if (@enc);
|
|
}
|
|
return (@resultArray) ? @resultArray : ();
|
|
} else {
|
|
die "illegal part definition\n";
|
|
}
|
|
return (); # if we got here, something went wrong...
|
|
}
|
|
|
|
sub encodeComplexType {
|
|
my $self=shift;
|
|
my $complexType=shift;
|
|
my $data = shift;
|
|
my @result = ();
|
|
my $schemaNS=$self->_wsdl_schemans ? $self->_wsdl_schemans .':' : '';
|
|
my $defaultNS=$self->_wsdl_tns;
|
|
my %nsHash = reverse %{ $self->_wsdl_ns };
|
|
#######################################################################
|
|
# TBD: handle restriction here
|
|
#######################################################################
|
|
### check for extension
|
|
my $extension=$complexType->find('.//'.$schemaNS.'extension')->shift;
|
|
if ($extension) {
|
|
### pull in extension base
|
|
my $base=$extension->findvalue('@base');
|
|
$base=~s/^$defaultNS\://;
|
|
$base=~s/^(.+?\:)//;
|
|
my $path;
|
|
{ no warnings;
|
|
# there are two ways how schema are usually defined
|
|
$path='/'.$self->_wsdl_wsdlns.'definitions/'
|
|
.$self->_wsdl_wsdlns."types/".$schemaNS."schema/"
|
|
.$schemaNS."complexType[\@name='$base']/descendant::".$schemaNS."element"
|
|
.'|'
|
|
.'/'.$self->_wsdl_wsdlns.'definitions/'
|
|
.$self->_wsdl_wsdlns."types/schema[\@xmlns='". $nsHash{$schemaNS} ."' and \@targetNameSpace = '". $nsHash{$1}."' ]/"
|
|
."complexType[\@name='$base']/descendant::element";
|
|
};
|
|
|
|
# got an absolute path - try cache first
|
|
my $elements=$self->{_WSDL}->{ caching } ? $self->{_WSDL}->{ cache }->{ $path } : undef;
|
|
|
|
unless ($elements) {
|
|
$elements=$self->{_WSDL}->{xpath}->find($path)
|
|
|| die "Error processing WSDL: '$path' not found";
|
|
# set cache
|
|
$self->{_WSDL}->{ cache }->{ $path } = $elements if ($self->{_WSDL}->{ caching });
|
|
};
|
|
|
|
while (my $e=$elements->shift) {
|
|
my @enc;
|
|
@enc=$self->encode($e, $data);
|
|
push @result, @enc if (@enc);
|
|
}
|
|
}
|
|
my $path='.//'.$schemaNS.'element';
|
|
my $elements=$complexType->find($path)
|
|
|| die "Error processing WSDL: './/".$schemaNS."element' not found";
|
|
|
|
while (my $e=$elements->shift) {
|
|
my @enc;
|
|
@enc=$self->encode($e, $data); # || die "$@";
|
|
push @result, @enc if (@enc);
|
|
}
|
|
return (@result) ? \SOAP::Data->value(@result) : ();
|
|
}
|
|
|
|
1;
|
|
|
|
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
SOAP::WSDL
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use SOAP::WSDL;
|
|
|
|
my $soap=SOAP::WSDL->new( wsdl => 'http://server.com/ws.wsdl' )
|
|
->proxy( 'http://myurl.com');
|
|
|
|
$soap->wsdlinit;
|
|
|
|
my $som=$soap->call( 'method' => [
|
|
{ name => value },
|
|
{ name => 'value' } ]);
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
SOAP::WSDL provides decent WSDL support for SOAP::Lite. It is built as a
|
|
add-on to SOAP::Lite, and will sit on top of it, forwarding all
|
|
the actual request-response to SOAP::Lite - somewhat like a pre-processor.
|
|
|
|
WSDL support means that you don't have to deal with those bitchy namespaces
|
|
some web services set on each and every method call parameter.
|
|
|
|
It also means an end to that nasty
|
|
|
|
SOAP::Data->name( 'Name' )->value(
|
|
SOAP::Data->name( 'Sub-Name')->value( 'Subvalue' )
|
|
);
|
|
|
|
encoding of complex data. (Another solution for this problem is just iterating
|
|
recursively over your data. But that doesn't work if you need more information
|
|
[e.g. namespaces etc] than just your data to encode your parameters).
|
|
|
|
And it means that you can use ordinary hashes for your parameters - the encording
|
|
order will be derived from the WSDL and not from your (unordered) data, thus the
|
|
problem of unordered perl-hashes and WSDL E<gt>sequenceE<lt> definitions is
|
|
solved, too. (Another solution for the ordering problem is tying your hash to a
|
|
class that provides ordered hashes - Tie::IxHash is one of them).
|
|
|
|
=head2 Why should I use this ?
|
|
|
|
SOAP::WSDL eases life for webservice developers who have to communicate with
|
|
lots of different web services using a reasonable big number of method calls.
|
|
|
|
If you just want to call a hand full of methods of one web service, take
|
|
SOAP::Lite's stubmaker and modify the stuff by hand if it doesn't work right
|
|
from the start. The overhead SOAP::WSDL imposes on your calls is not worth
|
|
the time saving.
|
|
|
|
If you need to access many web services offering zillions of methods to you,
|
|
this module should be your choice. It automatically encodes your perl data
|
|
structures correctly, based on the service's WSDL description, handling
|
|
even those complex types SOAP::Lite can't cope with.
|
|
|
|
SOAP::WSDL also eliminates most perl E<lt>-E<gt> .NET interoperability
|
|
problems by qualifying method and parameters as they are specified in the
|
|
WSDL definition.
|
|
|
|
=head1 USAGE
|
|
|
|
my $soap=SOAP::WSDL->new( wsdl => 'http://server.com/ws.wsdl' );
|
|
|
|
# or
|
|
my $soap=SOAP::WSDL->new()
|
|
$soap->wsdl('http://server.com/ws.wsdl');
|
|
|
|
# or
|
|
# without dispatching calls to the WebService
|
|
#
|
|
# useful for testing
|
|
my $soap=SOAP::WSDL->new( wsdl => 'http://server.com/ws.wsdl',
|
|
no_dispatch => 1 );
|
|
|
|
# you must call proxy before you call wsdlinit
|
|
$soap->proxy( 'http://myurl.com')
|
|
|
|
# optional (only necessary if the service you use is not the first one
|
|
# in document order in the WSDL file).
|
|
# If used, it must be called before wsdlinit.
|
|
$soap->servicename('Service1');
|
|
|
|
# optional, set to a false value if you don't want your
|
|
# soap message elements to be typed
|
|
$soap->autotype(0);
|
|
|
|
# never forget to call this !
|
|
$soap->wsdlinit;
|
|
|
|
# with caching enabled:
|
|
$soap->wsdlinit( caching => 1);
|
|
|
|
my $som=$soap->call( 'method' ,
|
|
name => 'value' ,
|
|
name => 'value' );
|
|
|
|
|
|
=head1 How it works
|
|
|
|
SOAP::WSDL takes the wsdl file specified and looks up the port for the service.
|
|
On calling a SOAP method, it looks up the message encoding and wraps all the
|
|
stuff around your data accordingly.
|
|
|
|
Most pre-processing is done in I<wsdlinit>, the rest is done in I<call>, which
|
|
overrides the same method from SOAP::Lite.
|
|
|
|
=head2 wsdlinit
|
|
|
|
SOAP::WSDL loads the wsdl file specified by the wsdl parameter / call using
|
|
SOAP::Lite's schema method. It sets up a XPath object of that wsdl file, and
|
|
subsequently queries it for namespaces, service, and port elements.
|
|
|
|
The port you are using is deduced from the URL you're going to
|
|
connect to. SOAP::WSDL uses the first service (in document order) specified
|
|
in the wsdl file. If you want to chose a different one, you can specify
|
|
the service by calling
|
|
|
|
$soap->servicename('ServiceToUse');
|
|
|
|
If you want to specify a service name, do it before calling I<wsdlinit> - it
|
|
has no effect afterwards.
|
|
|
|
=head2 call
|
|
|
|
The call method examines the wsdl file to find out how to encode the SOAP
|
|
message for your method. Lookups are done in real-time using XPath, so this
|
|
incorporates a small delay to your calls (see L</Memory consumption and performance>
|
|
below.
|
|
|
|
The SOAP message will include the types for each element, unless you have
|
|
set autotype to a false value by calling
|
|
|
|
$soap->autotype(0);
|
|
|
|
After wrapping your call into what is appropriate, SOAP::WSDL uses the I<call()>
|
|
method from SOAP::Lite to dispatch your call.
|
|
|
|
call takes the method name as first argument, and the parameters passed to that
|
|
method as following arguments.
|
|
|
|
B<Example:>
|
|
|
|
$som=$soap->call( "SomeMethod" => "test" => "testvalue" );
|
|
|
|
$som=$soap->call( "SomeMethod" => %args );
|
|
|
|
=head1 Caching
|
|
|
|
SOAP::WSDL uses a two-stage caching mechanism to achieve best performance.
|
|
|
|
First, there's a pretty simple caching mechanisms for storing XPath query results.
|
|
They are just stored in a hash with the XPath path as key (until recently, only
|
|
results of "find" or "findnodes" are cached). I did not use the obvious
|
|
L<Cache|Cache> or L<Cache::Cache|Cache::Cache> module here, because these use L<Storable|Storable> to
|
|
store complex objects and thus incorporate a performance loss heavier than using
|
|
no cache at all.
|
|
Second, the XPath object and the XPath results cache are be stored on disk using
|
|
the L<Cache::FileCache|Cache::FileCache> implementation.
|
|
|
|
A filesystem cache is only used if you
|
|
|
|
1) enable caching
|
|
2) set wsdl_cache_directory
|
|
|
|
The cache directory must be, of course, read- and writeable.
|
|
|
|
XPath result caching doubles performance, but increases memory consumption - if you lack of
|
|
memory, you should not enable caching (disabled by default).
|
|
|
|
Filesystem caching triples performance for wsdlinit and doubles performance for the first
|
|
method call.
|
|
|
|
The file system cache is written to disk when the SOAP::WSDL object is destroyed.
|
|
It may be written to disk any time by calling the L</wsdl_cache_store> method
|
|
|
|
Using both filesystem and in-memory caching is recommended for best performance and
|
|
smallest startup costs.
|
|
|
|
=head2 Sharing cache between applications
|
|
|
|
Sharing a file system cache among applications accessing the same web service is
|
|
generally possible, but may under some circumstances reduce performance, and under
|
|
some special circumstances even lead to errors.
|
|
This is due to the cache key algorithm used.
|
|
|
|
SOAP::WSDL uses the SOAP endpoint URL to store the XML::XPath object of the wsdl file.
|
|
In the rare case of a web service listening on one particular endpoint (URL) but using
|
|
more than one WSDL definition, this may lead to errors when applications using
|
|
SOAP::WSDL share a file system cache.
|
|
|
|
SOAP::WSDL stores the XPath results in-memory-cache in the filesystem cache, using the
|
|
key of the wsdl file with C<_cache> appended. Two applications sharing the file system
|
|
cache and accessing different methods of one web service could overwrite each others
|
|
in-memory-caches when dumping the XPath results to disk, resulting in a slight performance
|
|
drawback (even though this only happens in the rare case of one app being started before
|
|
the other one has had a chance to write its cache to disk).
|
|
|
|
=head2 Controlling the file system cache
|
|
|
|
If you want full controll over the file system cache, you can use wsdl_init_cash to
|
|
initialize it. wsdl_init_cash will take the same parameters as Cache::FileCache->new().
|
|
See L<Cache::Cache> and L<Cache::FileCache> for details.
|
|
|
|
=head2 Notes
|
|
|
|
If you plan to write your own caching implementation, you should consider the following:
|
|
|
|
The XPath results cache must not survive the XPath object SOAP::WSDL uses to
|
|
store the WSDL file in (this could cause memory holes - see L<XPath|XPath> for details).
|
|
This never happens during normal usage - but note that you have been warned
|
|
before trying to store and re-read SOAP::WSDL's internal cache.
|
|
|
|
=head1 Methods
|
|
|
|
=over 4
|
|
|
|
=item call
|
|
|
|
$soap->call($method, %data);
|
|
|
|
See above.
|
|
|
|
call will die if it can't find required elements in the WSDL file or if your data
|
|
doesn't meet the WSDL definition's requirements, so you might want to eval{} it.
|
|
On death, $@ will (hopefully) contain some error message like
|
|
|
|
Error processing WSDL: no <definitions> element found
|
|
|
|
to give you a hint about what went wrong.
|
|
|
|
=item no_dispatch
|
|
|
|
Gets/Sets the I<no_dispatch> flag. If no_dispatch is set to true value, SOAP::WSDL
|
|
will not dispatch your calls to a remote server but return the SOAP::SOM object
|
|
containing the call instead.
|
|
|
|
Useful for testing / debugging
|
|
|
|
=item encode
|
|
|
|
# this is how call uses encode
|
|
# $xpath contains a XPath object of the wsdl document
|
|
|
|
my $def=$xpath->find("/definitions")->shift;
|
|
my $parts=$def->find("//message[\@name='$messageName']/part");
|
|
|
|
my @param=();
|
|
|
|
while (my $part=$parts->shift) {
|
|
my $enc=$self->encode($part, \%data);
|
|
push @param, $enc if defined $enc;
|
|
}
|
|
|
|
Does the actual encoding. Expects a XPath::NodeSet as first, a hashref containing
|
|
your data as second parameter. The XPath nodeset must be a node specifying a WSDL
|
|
message part.
|
|
|
|
You won't need to call I<encode> unless you plan to
|
|
override I<call> or want to write a new SOAP server implementation.
|
|
|
|
=item servicename
|
|
|
|
$soap->servicename('Service1');
|
|
|
|
Use this to specify a service by name (if none specified, the first one in document
|
|
order from the WSDL file is used). You must call this before calling L</wsdlinit>.
|
|
|
|
=item wsdl
|
|
|
|
$soap->wsdl('http://my.web.service.com/wsdl');
|
|
|
|
Use this to specify the WSDL file to use. Must be a valid (and accessible !) url.
|
|
You must call this before calling L</wsdlinit>.
|
|
|
|
For time saving's sake, this should be a local file - you never know how much
|
|
time your WebService needs for delivering a wsdl file.
|
|
|
|
=item wsdlinit
|
|
|
|
$soap->wsdlinit( caching => 1,
|
|
cache_directory => '/tmp/cache' );
|
|
|
|
Initializes the WSDL document for usage and looks up the web service and port to use
|
|
(the port is derived by the URL specified via SOAP::Lite's I<proxy> method).
|
|
|
|
wsdlinit will die if it can't set up the WSDL file properly, so you might want to
|
|
eval{} it.
|
|
|
|
On death, $@ will (hopefully) contain some error message like
|
|
|
|
Error processing WSDL: no <definitions> element found
|
|
|
|
to give you a hint about what went wrong.
|
|
|
|
wsdlinit will accept a hash of parameters with the following keys:
|
|
|
|
=over 8
|
|
|
|
=item * caching
|
|
|
|
enables caching is set to a true value
|
|
|
|
=item * cache_directory
|
|
|
|
enables filesystem caching (in the directory specified). The directory given must be
|
|
existant, read- and writeable.
|
|
|
|
=back
|
|
|
|
=item wsdl_cache_directory
|
|
|
|
$soap->wsdl_cache_directory( '/tmp/cache' );
|
|
|
|
Sets the directory used for filesystem caching and enables filesystem caching.
|
|
Passing the I<cache_directory> parameter to wsdlinit has the same effect.
|
|
|
|
=item wsdl_cache_store
|
|
|
|
$soap->wsdl_cache_store();
|
|
|
|
Stores the content of the in-memory-cache (and the XML::XPath representation of
|
|
the WSDL file) to disk. This will not have any effect if cache_directory is not set.
|
|
|
|
=back
|
|
|
|
=head1 Notes
|
|
|
|
=head2 Why another SOAP module ?
|
|
|
|
SOAP::Lite provides only some rudimentary WSDL support. This lack is not just
|
|
something unimplemented, but an offspring of the SOAP::Schema
|
|
class design. SOAP::Schema uses some complicated format to store XML Schema information
|
|
(mostly a big hashref, containing arrays of SOAP::Data and a SOAP::Parser-derived
|
|
object). This data structure makes it pretty hard to improve SOAP::Lite's
|
|
WSDL support.
|
|
|
|
SOAP::WSDL uses XPath for processing WSDL. XPath is a query language standard for
|
|
XML, and usually a good choice for XML transformations or XML template processing
|
|
(and what else is WSDL-based en-/decoding ?). Besides, there's an excellent XPath
|
|
module (L<XML::XPath>) available from CPAN, and as SOAP::Lite uses XPath to
|
|
access elements in SOAP::SOM objects, this seems like a natural choice.
|
|
|
|
Fiddling the kind of WSDL support implemented here into SOAP::Lite would mean
|
|
a larger set of changes, so I decided to build something to use as add-on.
|
|
|
|
=head2 Memory consumption and performance
|
|
|
|
SOAP::WSDL uses around twice the memory (or even more) SOAP::Lite uses for the
|
|
same task (but remember: SOAP::WSDL does things for you SOAP::Lite can't).
|
|
It imposes a slight delay for initialization, and for every SOAP method call, too.
|
|
|
|
On my 1.4 GHz Pentium mobile notebook, the init delay with a simple
|
|
WSDL file (containing just one operation and some complex types and elements)
|
|
was around 50 ms, the delay for the first call around 25 ms and for subsequent
|
|
calls to the same method around 7 ms without and around 6 ms with XPath result caching
|
|
(on caching, see above). XML::XPath must do some caching, too - don't know where
|
|
else the speedup should come from.
|
|
|
|
Calling a method of a more complex WSDL file (defining around 10 methods and
|
|
numerous complex types on around 500 lines of XML), the delay for the first
|
|
call was around 100 ms for the first and 70 ms for subsequent method calls.
|
|
wsdlinit took around 150 ms to process the stuff. With XPath result caching enabled,
|
|
all but the first call take around 35 ms.
|
|
|
|
Using SOAP::WSDL on an idiotically complex WSDL file with just one method, but around
|
|
100 parameters for that method, mostly made up by extensions of complex types
|
|
(the heaviest XPath operation) takes around 1.2 s for the first call (0.9 with caching)
|
|
and around 830 ms for subsequent calls (arount 570 ms with caching).
|
|
|
|
The actual performance loss compared to SOAP::Lite should be around 10 % less
|
|
than the values above - SOAP::Lite encodes the data for you, too (or you do
|
|
it yourself) - and encoding in SOAP::WSDL is already covered by the pre-call
|
|
delay time mentioned above.
|
|
|
|
If you have lots of WebService methods and call each of them from time to time,
|
|
this delay should not affect your perfomance too much. If you have just one method
|
|
and keep calling it ever & ever again, you should cosider hardcoding your data
|
|
encoding (maybe even with hardcoded XML templates - yes, this may be a BIG speedup).
|
|
|
|
=head1 LIMITATIONS
|
|
|
|
=over
|
|
|
|
=item * E<lt>restrictionE<gt>
|
|
|
|
SOAP::WSDL doesn't handle C<restriction> WSDL elements yet.
|
|
|
|
=item * bindings
|
|
|
|
SOAP::WSDL does not care about port bindings yet.
|
|
|
|
=item * overloading
|
|
|
|
WSDL overloading is not supported yet.
|
|
|
|
=back
|
|
|
|
=head1 CAVEATS
|
|
|
|
=head2 API change between 1.13 and 1.14
|
|
|
|
The SOAP::WSDL API changed significantly between versions 1.13 and 1.14.
|
|
From 1.14 on, B<call> expects the following arguments: method name as scalar first,
|
|
method parameters as hash following.
|
|
|
|
The B<call> no longer recognizes the I<dispatch> option - to get the same behaviour,
|
|
pass C<no_dispatch => 1> to I<new> or call
|
|
|
|
$soap->no_dispatch(1);
|
|
|
|
=head2 Unstable interface
|
|
|
|
This is alpha software - everything may (and most things will) change.
|
|
But you don't have to be afraid too much - at least the I<call> synopsis should
|
|
be stable from 1.14 on, and that is the part you'll use most frequently.
|
|
|
|
=head1 BUGS
|
|
|
|
=over
|
|
|
|
=item * Check for the correct number of elements confused with complex types
|
|
|
|
If a complex type is marked optional in a WSDL file, but sub-parts are marked as
|
|
required, SOAP::WSDL will die if the complex type is not found in the data
|
|
(because it checks only for the occurence of simple type elements).
|
|
|
|
A quick-and-dirty workaround is to turn off the check with
|
|
|
|
$soap->wsdl_checkoccurs(0);
|
|
|
|
=item * Arrays of complex types are not checked for the correct number of elements
|
|
|
|
Arrays of complex types are just encoded and not checked for correctness etc.
|
|
I don't know if I do this right yet, but output looks good. However, they are not
|
|
checked for the correct number of element (does the SOAP spec say how to
|
|
specify this ?).
|
|
|
|
=item * +trace (and other SOAP::Lite flags) don't work
|
|
|
|
This may be an issue with older versions of the base module (before 2.?), or with
|
|
activestate's activeperl, which do
|
|
not call the base modules I<import> method with the flags supplied to the parent.
|
|
|
|
There's a simple workaround:
|
|
|
|
use SOAP::WSDL;
|
|
import SOAP::Lite +trace;
|
|
|
|
=item * nothing else known
|
|
|
|
But I'm sure there are some serious bugs lurking around somewhere.
|
|
|
|
=back
|
|
|
|
=head1 TODO
|
|
|
|
=over
|
|
|
|
=item Implement bindings support
|
|
|
|
WSDL bindings are required for SOAP authentication. This is not too
|
|
hard to implement - just look up bindings and decide for each
|
|
(top level) element whether to put it into header or body.
|
|
|
|
=item Allow use of alternative XPath implementations
|
|
|
|
XML::XPath is a great module, but it's not a race-winning one.
|
|
XML::LibXML offers a promising-looking XPath interface. SOAP::WSDL should
|
|
support both, defaulting to the faster one, and leaving the final choice
|
|
to the user.
|
|
|
|
=back
|
|
|
|
=head1 CHANGES
|
|
|
|
$Log: WSDL.pm,v $
|
|
Revision 1.18 2004/07/16 07:43:05 lsc
|
|
fixed test scripts for windows
|
|
|
|
Revision 1.17 2004/07/05 08:19:49 lsc
|
|
- added wsdl_checkoccurs
|
|
|
|
Revision 1.16 2004/07/04 09:01:14 lsc
|
|
- change <definitions> element lookup from find('/definitions') and find('wsdl:definitions') to find('/*[1]') to process arbitrary default (wsdl) namespaces correctly
|
|
- fixed test output in test 06
|
|
|
|
Revision 1.15 2004/07/02 12:28:31 lsc
|
|
- documentation update
|
|
- cosmetics
|
|
|
|
Revision 1.14 2004/07/02 10:53:36 lsc
|
|
- API change:
|
|
- call now behaves (almost) like SOAP::Lite::call
|
|
- call() takes a list (hash) as second argument
|
|
- call does no longer support the "dispatch" option
|
|
- dispatching calls can be suppressed by passing
|
|
"no_dispatch => 1" to new()
|
|
- dispatching calls can be suppressed by calling
|
|
$soap->no_dispatch(1);
|
|
and re-enabled by calling
|
|
$soap->no_dispatch(0);
|
|
- Updated test skripts to reflect API change.
|
|
|
|
Revision 1.13 2004/06/30 12:08:40 lsc
|
|
- added IServiceInstance (ecmed) to acceptance tests
|
|
- refined documentation
|
|
|
|
Revision 1.12 2004/06/26 14:13:29 lsc
|
|
- refined file caching
|
|
- added descriptive output to test scripts
|
|
|
|
Revision 1.11 2004/06/26 07:55:40 lsc
|
|
- fixed "freeze" caching bug
|
|
- improved test scripts to test file system caching (and show the difference)
|
|
|
|
Revision 1.10 2004/06/26 06:30:33 lsc
|
|
- added filesystem caching using Cache::FileCache
|
|
|
|
Revision 1.9 2004/06/24 12:27:23 lsc
|
|
Cleanup
|
|
|
|
Revision 1.8 2004/06/11 19:49:15 lsc
|
|
- moved .t files to more self-describing names
|
|
- changed WSDL.pm to accept AXIS wsdl files
|
|
- implemented XPath query result caching on all absolute queries
|
|
|
|
Revision 1.7 2004/06/07 13:01:16 lsc
|
|
added changelog to pod
|
|
|
|
|
|
=head1 COPYRIGHT
|
|
|
|
(c) 2004 Martin Kutter
|
|
|
|
This library is free software, you can use it under the same
|
|
terms as perl itself.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Martin Kutter <martin.kutter@fen-net.de>
|
|
|
|
=cut
|