From 8c05b81d6860d8e0da4098cc6f59f630d7c53037 Mon Sep 17 00:00:00 2001 From: Adam Frisby Date: Tue, 25 Mar 2008 16:29:54 +0000 Subject: * Committing Lulurun's Cgi/Perl implementation of the UGAI servers. * I love you long time. --- share/perl/README | 40 + share/perl/asset.cgi | 43 + share/perl/conf/httpd-vhosts.conf | 25 + share/perl/conf/mod_perl-startup.pl | 34 + share/perl/grid.cgi | 27 + share/perl/inventory.cgi | 39 + share/perl/lib/DBHandler.pm | 119 + share/perl/lib/MyCGI.pm | 91 + share/perl/lib/OpenSim/AssetServer.pm | 87 + share/perl/lib/OpenSim/AssetServer/AssetManager.pm | 34 + share/perl/lib/OpenSim/AssetServer/Config.pm | 24 + share/perl/lib/OpenSim/Config.pm | 41 + share/perl/lib/OpenSim/GridServer.pm | 208 ++ share/perl/lib/OpenSim/GridServer/Config.pm | 50 + share/perl/lib/OpenSim/GridServer/GridManager.pm | 57 + share/perl/lib/OpenSim/InventoryServer.pm | 249 ++ share/perl/lib/OpenSim/InventoryServer/Config.pm | 51 + .../OpenSim/InventoryServer/InventoryManager.pm | 86 + share/perl/lib/OpenSim/UserServer.pm | 239 ++ share/perl/lib/OpenSim/UserServer/Config.pm | 125 + share/perl/lib/OpenSim/UserServer/UserManager.pm | 49 + share/perl/lib/OpenSim/Utility.pm | 155 + share/perl/lib/XML/RPC.pm | 217 ++ share/perl/lib/XML/Serializer.pm | 163 + share/perl/lib/XML/Simple.pm | 3284 ++++++++++++++++++++ share/perl/lib/XML/TreePP.pm | 1228 ++++++++ share/perl/test/OpenSimTest.pm | 53 + share/perl/test/OpenSimTest/AssetTester.pm | 17 + share/perl/test/OpenSimTest/Config.pm | 53 + share/perl/test/OpenSimTest/GridTester.pm | 62 + share/perl/test/OpenSimTest/InventoryTester.pm | 116 + share/perl/test/OpenSimTest/UserTester.pm | 53 + share/perl/test/PerformanceTest.pl | 78 + share/perl/test/SingleTest.pl | 21 + share/perl/user.cgi | 28 + 35 files changed, 7246 insertions(+) create mode 100644 share/perl/README create mode 100644 share/perl/asset.cgi create mode 100644 share/perl/conf/httpd-vhosts.conf create mode 100644 share/perl/conf/mod_perl-startup.pl create mode 100644 share/perl/grid.cgi create mode 100644 share/perl/inventory.cgi create mode 100644 share/perl/lib/DBHandler.pm create mode 100644 share/perl/lib/MyCGI.pm create mode 100644 share/perl/lib/OpenSim/AssetServer.pm create mode 100644 share/perl/lib/OpenSim/AssetServer/AssetManager.pm create mode 100644 share/perl/lib/OpenSim/AssetServer/Config.pm create mode 100644 share/perl/lib/OpenSim/Config.pm create mode 100644 share/perl/lib/OpenSim/GridServer.pm create mode 100644 share/perl/lib/OpenSim/GridServer/Config.pm create mode 100644 share/perl/lib/OpenSim/GridServer/GridManager.pm create mode 100644 share/perl/lib/OpenSim/InventoryServer.pm create mode 100644 share/perl/lib/OpenSim/InventoryServer/Config.pm create mode 100644 share/perl/lib/OpenSim/InventoryServer/InventoryManager.pm create mode 100644 share/perl/lib/OpenSim/UserServer.pm create mode 100644 share/perl/lib/OpenSim/UserServer/Config.pm create mode 100644 share/perl/lib/OpenSim/UserServer/UserManager.pm create mode 100644 share/perl/lib/OpenSim/Utility.pm create mode 100644 share/perl/lib/XML/RPC.pm create mode 100644 share/perl/lib/XML/Serializer.pm create mode 100644 share/perl/lib/XML/Simple.pm create mode 100644 share/perl/lib/XML/TreePP.pm create mode 100644 share/perl/test/OpenSimTest.pm create mode 100644 share/perl/test/OpenSimTest/AssetTester.pm create mode 100644 share/perl/test/OpenSimTest/Config.pm create mode 100644 share/perl/test/OpenSimTest/GridTester.pm create mode 100644 share/perl/test/OpenSimTest/InventoryTester.pm create mode 100644 share/perl/test/OpenSimTest/UserTester.pm create mode 100644 share/perl/test/PerformanceTest.pl create mode 100644 share/perl/test/SingleTest.pl create mode 100644 share/perl/user.cgi (limited to 'share') diff --git a/share/perl/README b/share/perl/README new file mode 100644 index 0000000..28bd9bf --- /dev/null +++ b/share/perl/README @@ -0,0 +1,40 @@ +INTRODUCTION + +This is a Opensim UGAI server compatible implementation. +It is written in Perl, based on apache (CGI or mod_perl) +Not all of the functions of UGAI server are implemented, but currently, +it supports the whole user login process and most of the operations on +inventory, asset. + +The main purpose of this implemetation is to improve UGAI server's +* stability - That's what Apache was born to do +* scability - You can use reliable technology such as load balancing, + clustering that have been used for years. + +IMPLEMENTATION + +"*.cgi" are the server programs, for example of user server: +opensim -> http://127.0.0.1:8002 +here -> http://127.0.0.1/user.cgi + +"lib" includes library file (*.pm) used by cgis. +"test" includes testcases. Instructions are included in "*.pl" files. + +INSTALLNATION & CONFIGURATION + +* additional perl packages (Fedora, Suse, CentOS rpms available) + DBI + DBD::mysql + Data::UUID + +* A sample apache configuration file is included in "conf" + http-vhost.conf + mod_perl-startup.pl + +* lib/OpenSim/Config.pm need to be configured to fit your environment. + Please follow the comment in that file. + +CONTACT + +lulurun@gmail.com + diff --git a/share/perl/asset.cgi b/share/perl/asset.cgi new file mode 100644 index 0000000..318e06f --- /dev/null +++ b/share/perl/asset.cgi @@ -0,0 +1,43 @@ +#!/usr/bin/perl -w + +use strict; +use Carp; +use MyCGI; +use OpenSim::Config; +use OpenSim::Utility; +use OpenSim::AssetServer; + +# !! +# TODO: ERROR code +# +my $param = &MyCGI::getParam(); +my $response = ""; +if ($ENV{REQUEST_METHOD} eq "POST") { + my $request = $param->{'POSTDATA'}; + #&OpenSim::Utility::Log("asset", "request", $ENV{REQUEST_URI}, $request); + $response = &OpenSim::AssetServer::saveAsset($request); +} else { # get + eval { + my $rest_param = &getRestParam(); + #&OpenSim::Utility::Log("asset", "request", $ENV{REQUEST_URI}); + my $rest_param_count = @$rest_param; + if ($rest_param_count < 2) { + Carp::croak($OpenSim::Config::SYS_MSG{FATAL}); + } + $response = &OpenSim::AssetServer::getAsset($rest_param->[$#$rest_param], $param); + }; + if ($@) { + $response = "$@"; # TODO: better return message needed. + } +} +#&OpenSim::Utility::Log("asset", "response", $response); +&MyCGI::outputXml("utf-8", $response); + +sub getRestParam { + my $uri = $ENV{REQUEST_URI} || Carp::croak($OpenSim::Config::SYS_MSG{FATAL}); + my ($request_uri, undef) = split(/\?/, $uri); + $request_uri =~ s/\/$//; + my @param = split(/\//, $request_uri); + return \@param; +} + diff --git a/share/perl/conf/httpd-vhosts.conf b/share/perl/conf/httpd-vhosts.conf new file mode 100644 index 0000000..447150f --- /dev/null +++ b/share/perl/conf/httpd-vhosts.conf @@ -0,0 +1,25 @@ +LoadModule perl_module modules/mod_perl.so +PerlRequire "conf/mod_perl-startup.pl" + +NameVirtualHost *:80 + + ServerName opensim.lulu + ServerAdmin webmaster@opensim.lulu + DocumentRoot /home/lulu/temp/opensim + ErrorLog logs/opensim-error_log + CustomLog logs/opensim-access_log common + + + Options MultiViews All + AllowOverride None + Order allow,deny + Allow from all + + + + SetHandler perl-script + PerlResponseHandler ModPerl::Registry + PerlOptions +ParseHeaders + + + diff --git a/share/perl/conf/mod_perl-startup.pl b/share/perl/conf/mod_perl-startup.pl new file mode 100644 index 0000000..e8bdb2c --- /dev/null +++ b/share/perl/conf/mod_perl-startup.pl @@ -0,0 +1,34 @@ +# Taken from http://perl.apache.org/docs/2.0/user/handlers/server.html#Startup_File + +if ( ! $ENV{MOD_PERL}) { die "GATEWAY_INTERFACE not Perl!"; } + +# !!! set this to your opensim's lib +use lib qw(/home/lulu/temp/opensim/lib); + +# enable if the mod_perl 1.0 compatibility is needed +# use Apache2::compat (); + +# preload all mp2 modules +# use ModPerl::MethodLookup; +# ModPerl::MethodLookup::preload_all_modules(); + +use ModPerl::Util (); #for CORE::GLOBAL::exit + +use Apache2::RequestRec (); +use Apache2::RequestIO (); +use Apache2::RequestUtil (); + +use Apache2::ServerRec (); +use Apache2::ServerUtil (); +use Apache2::Connection (); +use Apache2::Log (); + +use APR::Table (); + +use ModPerl::Registry (); + +use Apache2::Const -compile => ':common'; +use APR::Const -compile => ':common'; + + +1; diff --git a/share/perl/grid.cgi b/share/perl/grid.cgi new file mode 100644 index 0000000..cf1550f --- /dev/null +++ b/share/perl/grid.cgi @@ -0,0 +1,27 @@ +#!/usr/bin/perl -w + +use strict; +use Carp; +use XML::RPC; +use MyCGI; +use OpenSim::Utility; +use OpenSim::GridServer; + +my $param = &MyCGI::getParam(); +my $request = $param->{'POSTDATA'}; +#&OpenSim::Utility::Log("grid", "request", $request); +my $xmlrpc = new XML::RPC(); +my $response = $xmlrpc->receive($request, \&XMLRPCHandler); +#&OpenSim::Utility::Log("grid", "response", $response); +&MyCGI::outputXml("utf-8", $response); + +sub XMLRPCHandler { + my ($methodname, @param) = @_; + my $handler_list = &OpenSim::GridServer::getHandlerList(); + if (!$handler_list->{$methodname}) { + Carp::croak("?"); + } else { + my $handler = $handler_list->{$methodname}; + $handler->(@param); + } +} diff --git a/share/perl/inventory.cgi b/share/perl/inventory.cgi new file mode 100644 index 0000000..0542436 --- /dev/null +++ b/share/perl/inventory.cgi @@ -0,0 +1,39 @@ +#!/usr/bin/perl -w + +use strict; +use MyCGI; +use OpenSim::Config; +use OpenSim::InventoryServer; +use Carp; + +my $request_uri = $ENV{REQUEST_URI} || Carp::croak($OpenSim::Config::SYS_MSG{FATAL}); +my $request_method = ""; +if ($request_uri =~ /([^\/]+)\/$/) { + $request_method = $1; +} else { + &MyCGI::outputXml("utf-8", $OpenSim::Config::SYS_MSG{FATAL}); +} +my $param = &MyCGI::getParam(); +my $post_data = $param->{'POSTDATA'}; +&OpenSim::Utility::Log("inv", "request", $request_uri, $post_data); +my $response = ""; +eval { + $response = &handleRequest($request_method, $post_data); +}; +if ($@) { + $response = "$@"; +} +&OpenSim::Utility::Log("inv", "response", $response); +&MyCGI::outputXml("utf-8", $response); + +sub handleRequest { + my ($methodname, $post_data) = @_; + my $handler_list = &OpenSim::InventoryServer::getHandlerList(); + if (!$handler_list->{$methodname}) { + Carp::croak("unknown method name"); + } else { + my $handler = $handler_list->{$methodname}; + return $handler->($post_data); + } +} + diff --git a/share/perl/lib/DBHandler.pm b/share/perl/lib/DBHandler.pm new file mode 100644 index 0000000..1435ba2 --- /dev/null +++ b/share/perl/lib/DBHandler.pm @@ -0,0 +1,119 @@ +use strict; +use DBI; +use Carp; + +package DBHandler; + +#our $dbh = undef; +use vars qw ($DB_CONNECTION); + +sub getConnection { + my ($dsn, $user, $pass) = @_; + #return $DB_CONNECTION if ($DB_CONNECTION); + $DB_CONNECTION = DBI->connect($dsn, $user, $pass); + $DB_CONNECTION->{AutoCommit} = 1; + $DB_CONNECTION->{RaiseError} = 1; + return $DB_CONNECTION; +} + +# ############# +# Simple statement +package Statement; + +sub new { + my ( $this, $dbh, $sql, $is_trans ) = @_; + # @@@ sql should be tested OK, so here just die + my $sth = $dbh->prepare($sql) || Carp::croak( $dbh->errstr ); + my %fields = ( + dbh => $dbh, + sql => $sql, + sth => $sth, + is_trans => $is_trans, + ); + return bless \%fields, $this; +} + +sub exec { + my ( $this, @param ) = @_; + my $dbh = $this->{dbh}; + my $sth = $this->{sth}; + my $sql = $this->{sql}; + + if ( !$sth->execute(@param) ) { + if ( $this->{is_trans} ) { + $dbh->rollback(); + } + Carp::croak( $dbh->errstr ); + } + my @ret = (); + if ( $sql =~ /^select/i ) { + # @@@ get result object + while ( my $res = $sth->fetchrow_hashref() ) { + push @ret, $res; + } + } + # @@@ $sth->finish(); + return \@ret; +} + +sub last_id { + my $this = shift; + my $dbh = $this->{dbh}; + return $dbh->last_insert_id(undef, undef, undef, undef); +} + +sub DESTROY { + my $this = shift; + my $sth = $this->{sth}; + $sth->finish(); +} + +# ############# +# Transaction +package Transaction; + +my $IS_TRANS = 1; + +sub new { + my ( $this, $dbh ) = @_; + # @@@ fatal error, just die + $dbh->begin_work() || Carp::croak( $dbh->errstr ); + my %fields = ( + dbh => $dbh, + Active => 1, + ); + return bless \%fields, $this; +} + +sub createStatement { + my ( $this, $sql) = @_; + # @@@ fatal error, just die + Carp::croak("transaction not begin") if ( !$this->{Active} ); + my $dbh = $this->{dbh}; + return new Statement($dbh, $sql, $IS_TRANS); +} + +sub commit { + my $this = shift; + my $dbh = $this->{dbh}; + if ( $this->{Active} && !$dbh->{AutoCommit} ) { + $dbh->commit || Carp::croak( $dbh->errstr ); + } + $this->{Active} = 0; +} + +sub rollback { + my $this = shift; + my $dbh = $this->{dbh}; + if ( $this->{Active} && !$dbh->{AutoCommit} ) { + $dbh->rollback || Carp::croak( $dbh->errstr ); + } + $this->{Active} = 0; +} + +sub DESTROY { + my $this = shift; + $this->rollback; +} + +1; diff --git a/share/perl/lib/MyCGI.pm b/share/perl/lib/MyCGI.pm new file mode 100644 index 0000000..1f232aa --- /dev/null +++ b/share/perl/lib/MyCGI.pm @@ -0,0 +1,91 @@ +package MyCGI; + +use strict; +use CGI; + +sub getParam { + my $cgi; + if ($ARGV[0]) { + $cgi = new CGI($ARGV[0]); + } else { + $cgi = new CGI; + } + my @param_names = $cgi->param(); + my %param = (); + foreach (@param_names) { + $param{$_} = $cgi->param($_); + } + return \%param; +} + +sub getCookie { + my $name = shift; + my $cookie_value = &CGI::cookie($name); + return &_parse($cookie_value); +} + +sub outputHtml { + my ($charset, $html) = @_; + print &CGI::header(-charset => $charset); + print $html; +} + +sub outputXml { + my ($charset, $xml) = @_; + print &CGI::header( -type => 'text/xml', -charset => $charset ); + print $xml; +} + +sub makeCookieValue { + my $param = shift; + my @data = (); + foreach(keys %$param) { + push(@data, $_ . "=" . $param->{$_}); + } + return join("&", @data); +} + +sub setCookie { + my $param = shift; + my $cookie = &CGI::cookie( + -name => $param->{name} || return, + -value => $param->{value}, + -domain => $param->{domain}, + -path => $param->{path}, + -expires => $param->{expires}, + ); + return &CGI::header(-cookie => $cookie); +} + +sub redirect { + my $dest = shift; + &CGI::redirect($dest); +} + +sub urlEncode { + my $str = shift; + $str =~ s/([^\w ])/'%'.unpack('H2', $1)/eg; + $str =~ tr/ /+/; + return $str; +} + +sub urlDecode { + my $str = shift; + $str =~ tr/+/ /; + $str =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('H2', $1)/eg; + return $str; +} + +sub _parse { + my $value = shift; + my @pair = split(/&/, $value); + my %data = (); + foreach(@pair) { + my ($name, $value) = split(/=/, $_); + $data{$name} = $value; + } + return \%data; +} + +1; + diff --git a/share/perl/lib/OpenSim/AssetServer.pm b/share/perl/lib/OpenSim/AssetServer.pm new file mode 100644 index 0000000..6418166 --- /dev/null +++ b/share/perl/lib/OpenSim/AssetServer.pm @@ -0,0 +1,87 @@ +package OpenSim::AssetServer; + +use strict; +use MIME::Base64; +use XML::Simple; +use OpenSim::Utility; +use OpenSim::AssetServer::AssetManager; + +# !! +# TODO: delete asset +# + +sub getAsset { + my ($asset_id, $param) = @_; + # get asset + my $asset_id_string = &OpenSim::Utility::UUID2HEX($asset_id); + my $asset = &OpenSim::AssetServer::AssetManager::getAssetByUUID($asset_id_string); + $asset->{assetUUID} = $asset_id; + # make response + return &_asset_to_xml($asset); +} + +sub saveAsset { + my $xml = shift; + my $asset = &_xml_to_asset($xml); + &OpenSim::AssetServer::AssetManager::saveAsset($asset); + return ""; # TODO: temporary solution of "success!" +} + +# ################## +# private functions +sub _asset_to_xml { + my $asset = shift; + my $asset_data = &MIME::Base64::encode_base64($asset->{data}); + return << "ASSET_XML"; + + +$asset_data + + + $asset->{assetUUID} + + $asset->{assetType} + $asset->{invType} + $asset->{name} + $asset->{description} + $asset->{local} + $asset->{temporary} + +ASSET_XML +} + +sub _xml_to_asset { + my $xml = shift; + my $xs = new XML::Simple(); + my $obj = $xs->XMLin($xml); +print STDERR $obj->{FullID}->{UUID} . "\n"; + my %asset = ( + "id" => &OpenSim::Utility::UUID2BIN($obj->{FullID}->{UUID}), + "name" => $obj->{Name}, + "description" => $obj->{Description}, + "assetType" => $obj->{Type}, + "invType" => $obj->{InvType}, + "local" => $obj->{Local}, + "temporary" => $obj->{Temporary}, + "data" => &MIME::Base64::decode_base64($obj->{Data}), + ); + return \%asset; +} + +1; + +__END__ + +{ + Data => "PFNjZW5lT2JqZWN0R3JvdXA+PFJvb3RQYXJ0PjxTY2VuZU9iamVjdFBhcnQgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PExhc3RPd25lcklEPjxVVUlEPmI5Y2I1OGU4LWYzYzktNGFmNS1iZTQ3LTAyOTc2MmJhYTY4ZjwvVVVJRD48L0xhc3RPd25lcklEPjxPd25lcklEPjxVVUlEPmI5Y2I1OGU4LWYzYzktNGFmNS1iZTQ3LTAyOTc2MmJhYTY4ZjwvVVVJRD48L093bmVySUQ+PEdyb3VwSUQ+PFVVSUQ+MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwPC9VVUlEPjwvR3JvdXBJRD48T3duZXJzaGlwQ29zdD4wPC9Pd25lcnNoaXBDb3N0PjxPYmplY3RTYWxlVHlwZT4wPC9PYmplY3RTYWxlVHlwZT48U2FsZVByaWNlPjA8L1NhbGVQcmljZT48Q2F0ZWdvcnk+MDwvQ2F0ZWdvcnk+PENyZWF0aW9uRGF0ZT4xMTk4NjQ5MjA5PC9DcmVhdGlvbkRhdGU+PFBhcmVudElEPjA8L1BhcmVudElEPjxPd25lck1hc2s+NTI2MDUzNjkyPC9Pd25lck1hc2s+PE5leHRPd25lck1hc2s+MjU3NDg3MTMyPC9OZXh0T3duZXJNYXNrPjxHcm91cE1hc2s+MDwvR3JvdXBNYXNrPjxFdmVyeW9uZU1hc2s+MDwvRXZlcnlvbmVNYXNrPjxCYXNlTWFzaz4yMTQ3NDgzNjQ3PC9CYXNlTWFzaz48Q3JlYXRvcklEPjxVVUlEPmI5Y2I1OGU4LWYzYzktNGFmNS1iZTQ3LTAyOTc2MmJhYTY4ZjwvVVVJRD48L0NyZWF0b3JJRD48VVVJRD48VVVJRD5hMGY3NmQzYi02MTlkLTRjNjktODVmOS0zNzhjMDExZDg2NzI8L1VVSUQ+PC9VVUlEPjxMb2NhbElEPjcwMjAwMTwvTG9jYWxJRD48TmFtZT5QcmltaXRpdmU8L05hbWU+PE9iamVjdEZsYWdzPjY1NjY2PC9PYmplY3RGbGFncz48TWF0ZXJpYWw+MDwvTWF0ZXJpYWw+PFJlZ2lvbkhhbmRsZT4xMDk5NTExNjI4MDMyMDAwPC9SZWdpb25IYW5kbGU+PEdyb3VwUG9zaXRpb24+PFg+MTMwLjA5OTQ8L1g+PFk+MTI4LjcxNTQ8L1k+PFo+MjEuMzM1NTI8L1o+PC9Hcm91cFBvc2l0aW9uPjxPZmZzZXRQb3NpdGlvbj48WD4wPC9YPjxZPjA8L1k+PFo+MDwvWj48L09mZnNldFBvc2l0aW9uPjxSb3RhdGlvbk9mZnNldD48WD4wPC9YPjxZPjA8L1k+PFo+MDwvWj48Vz4xPC9XPjwvUm90YXRpb25PZmZzZXQ+PFZlbG9jaXR5PjxYPjA8L1g+PFk+MDwvWT48Wj4wPC9aPjwvVmVsb2NpdHk+PFJvdGF0aW9uYWxWZWxvY2l0eT48WD4wPC9YPjxZPjA8L1k+PFo+MDwvWj48L1JvdGF0aW9uYWxWZWxvY2l0eT48QW5ndWxhclZlbG9jaXR5PjxYPjA8L1g+PFk+MDwvWT48Wj4wPC9aPjwvQW5ndWxhclZlbG9jaXR5PjxBY2NlbGVyYXRpb24+PFg+MDwvWD48WT4wPC9ZPjxaPjA8L1o+PC9BY2NlbGVyYXRpb24+PERlc2NyaXB0aW9uIC8+PENvbG9yIC8+PFRleHQgLz48U2l0TmFtZSAvPjxUb3VjaE5hbWUgLz48TGlua051bT4wPC9MaW5rTnVtPjxDbGlja0FjdGlvbj4wPC9DbGlja0FjdGlvbj48U2hhcGU+PFN0YXRlPjA8L1N0YXRlPjxQQ29kZT45PC9QQ29kZT48UGF0aEJlZ2luPjA8L1BhdGhCZWdpbj48UGF0aEVuZD4wPC9QYXRoRW5kPjxQYXRoU2NhbGVYPjIwMDwvUGF0aFNjYWxlWD48UGF0aFNjYWxlWT4yMDA8L1BhdGhTY2FsZVk+PFBhdGhTaGVhclg+MDwvUGF0aFNoZWFyWD48UGF0aFNoZWFyWT4wPC9QYXRoU2hlYXJZPjxQYXRoU2tldz4wPC9QYXRoU2tldz48UHJvZmlsZUJlZ2luPjA8L1Byb2ZpbGVCZWdpbj48UHJvZmlsZUVuZD4wPC9Qcm9maWxlRW5kPjxTY2FsZT48WD4wLjU8L1g+PFk+MC41PC9ZPjxaPjAuNTwvWj48L1NjYWxlPjxQYXRoQ3VydmU+MTY8L1BhdGhDdXJ2ZT48UHJvZmlsZUN1cnZlPjA8L1Byb2ZpbGVDdXJ2ZT48UHJvZmlsZUhvbGxvdz4wPC9Qcm9maWxlSG9sbG93PjxQYXRoUmFkaXVzT2Zmc2V0PjA8L1BhdGhSYWRpdXNPZmZzZXQ+PFBhdGhSZXZvbHV0aW9ucz4wPC9QYXRoUmV2b2x1dGlvbnM+PFBhdGhUYXBlclg+MDwvUGF0aFRhcGVyWD48UGF0aFRhcGVyWT4wPC9QYXRoVGFwZXJZPjxQYXRoVHdpc3Q+MDwvUGF0aFR3aXN0PjxQYXRoVHdpc3RCZWdpbj4wPC9QYXRoVHdpc3RCZWdpbj48VGV4dHVyZUVudHJ5PkFBQUFBQUFBQUFDWm1RQUFBQUFBQlFBQUFBQUFBQUFBZ0Q4QUFBQ0FQd0FBQUFBQUFBQUFBQUFBQUFBPTwvVGV4dHVyZUVudHJ5PjxFeHRyYVBhcmFtcz5BQT09PC9FeHRyYVBhcmFtcz48UHJvZmlsZVNoYXBlPkNpcmNsZTwvUHJvZmlsZVNoYXBlPjwvU2hhcGU+PFNjYWxlPjxYPjAuNTwvWD48WT4wLjU8L1k+PFo+MC41PC9aPjwvU2NhbGU+PFVwZGF0ZUZsYWc+MDwvVXBkYXRlRmxhZz48L1NjZW5lT2JqZWN0UGFydD48L1Jvb3RQYXJ0PjxPdGhlclBhcnRzIC8+PC9TY2VuZU9iamVjdEdyb3VwPgA=", + Description => {}, + FullID => { UUID => "feb7e249-e462-499f-a881-553b9829539a" }, + InvType => 6, + Local => "false", + Name => "Primitive", + Temporary => "false", + Type => 6, + "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema", + "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance", +} + diff --git a/share/perl/lib/OpenSim/AssetServer/AssetManager.pm b/share/perl/lib/OpenSim/AssetServer/AssetManager.pm new file mode 100644 index 0000000..f36ab1a --- /dev/null +++ b/share/perl/lib/OpenSim/AssetServer/AssetManager.pm @@ -0,0 +1,34 @@ +package OpenSim::AssetServer::AssetManager; + +use strict; +use Carp; +use OpenSim::Utility; +use OpenSim::AssetServer::Config; + + +sub getAssetByUUID { + my $uuid = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::AssetServer::Config::SYS_SQL{select_asset_by_uuid}, $uuid); + my $count = @$result; + if ($count > 0) { + return $result->[0]; + } + Carp::croak("can not find asset($uuid)"); +} + +sub saveAsset { + my $asset = shift; + my $result = &OpenSim::Utility::getSimpleResult( + $OpenSim::AssetServer::Config::SYS_SQL{insert_asset}, + $asset->{id}, + $asset->{name}, + $asset->{description}, + $asset->{assetType}, + $asset->{invType}, + $asset->{"local"}, + $asset->{temporary}, + $asset->{data} + ); +} + +1; diff --git a/share/perl/lib/OpenSim/AssetServer/Config.pm b/share/perl/lib/OpenSim/AssetServer/Config.pm new file mode 100644 index 0000000..5598921 --- /dev/null +++ b/share/perl/lib/OpenSim/AssetServer/Config.pm @@ -0,0 +1,24 @@ +package OpenSim::AssetServer::Config; + +use strict; + +our %SYS_SQL = ( + select_asset_by_uuid => + "SELECT * FROM assets WHERE id=X?", + insert_asset => + "INSERT INTO assets VALUES (?,?,?,?,?,?,?,?)" +); + + +our @ASSETS_COLUMNS = ( + "id", + "name", + "description", + "assetType", + "invType", + "local", + "temporary", + "data", +); + +1; diff --git a/share/perl/lib/OpenSim/Config.pm b/share/perl/lib/OpenSim/Config.pm new file mode 100644 index 0000000..246ef26 --- /dev/null +++ b/share/perl/lib/OpenSim/Config.pm @@ -0,0 +1,41 @@ +package OpenSim::Config; + +# REGION keys +our $SIM_RECV_KEY = ""; +our $SIM_SEND_KEY = ""; +# ASSET server url +#our $ASSET_SERVER_URL = "http://127.0.0.1:8003/"; +our $ASSET_SERVER_URL = "http://opensim.wolfdrawer.net:80/asset.cgi"; +our $ASSET_RECV_KEY = ""; +our $ASSET_SEND_KEY = ""; +# USER server url +#our $USER_SERVER_URL = "http://127.0.0.1:8001/"; +our $USER_SERVER_URL = "http://opensim.wolfdrawer.net:80/user.cgi"; +our $USER_RECV_KEY = ""; +our $USER_SEND_KEY = ""; +# GRID server url +#our $GRID_SERVER_URL = "http://127.0.0.1:8001/"; +our $GRID_SERVER_URL = "http://opensim.wolfdrawer.net:80/grid.cgi"; +our $GRID_RECV_KEY = ""; +our $GRID_SEND_KEY = ""; +# INVENTORY server url +#our $INVENTORY_SERVER_URL = "http://127.0.0.1:8004"; +our $INVENTORY_SERVER_URL = "http://opensim.wolfdrawer.net:80/inventory.cgi"; +# DB +our $DSN = "dbi:mysql:database=opensim;host=192.168.0.20"; +our $DBUSER = "lulu"; +our $DBPASS = "1234"; + +# DEBUG LOG +our $DEBUG_LOGDIR = "/home/lulu/temp/opensim"; + +# MSG +our %SYS_MSG = ( + FATAL => "You must have been eaten by a wolf.", + FAIL => "Late! There is a wolf behind you", + LOGIN_WELCOME => "Do you fear the wolf ?", +); + + +1; + diff --git a/share/perl/lib/OpenSim/GridServer.pm b/share/perl/lib/OpenSim/GridServer.pm new file mode 100644 index 0000000..7b21cd8 --- /dev/null +++ b/share/perl/lib/OpenSim/GridServer.pm @@ -0,0 +1,208 @@ +package OpenSim::GridServer; + +use strict; +use OpenSim::Utility; +use OpenSim::GridServer::Config; +use OpenSim::GridServer::GridManager; + +sub getHandlerList { + my %list = ( + "simulator_login" => \&_simulator_login, + "simulator_data_request" => \&_simulator_data_request, + "map_block" => \&_map_block, + "map_block2" => \&_map_block2, # this is better for the Region Monitor + ); + return \%list; +} + +# ################# +# XMLRPC Handlers +sub _simulator_login { + my $params = shift; + + my $region_data = undef; + my %response = (); + if ($params->{"UUID"}) { + $region_data = &OpenSim::GridServer::GridManager::getRegionByUUID($params->{"UUID"}); + } elsif ($params->{"region_handle"}) { + $region_data = &OpenSim::GridServer::GridManager::getRegionByHandle($params->{"region_handle"}); + } else { + $response{"error"} = "No UUID or region_handle passed to grid server - unable to connect you"; + return \%response; + } + + if (!$region_data) { + my %new_region_data = ( + uuid => undef, + regionHandle => OpenSim::Utility::UIntsToLong($params->{region_locx}*256, $params->{region_locx}*256), + regionName => $params->{sim_name}, + regionRecvKey => $OpenSim::Config::SIM_RECV_KEY, + regionSendKey => $OpenSim::Config::SIM_SEND_KEY, + regionSecret => $OpenSim::Config::SIM_RECV_KEY, + regionDataURI => "", + serverIP => $params->{sim_ip}, + serverPort => $params->{sim_port}, + serverURI => "http://" + $params->{sim_ip} + ":" + $params->{sim_port} + "/", + LocX => $params->{region_locx}, + LocY => $params->{region_locy}, + LocZ => 0, + regionAssetURI => $OpenSim::Config::ASSET_SERVER_URL, + regionAssetRecvKey => $OpenSim::Config::ASSET_RECV_KEY, + regionAssetSendKey => $OpenSim::Config::ASSET_SEND_KEY, + regionUserURI => $OpenSim::Config::USER_SERVER_URL, + regionUserRecvKey => $OpenSim::Config::USER_RECV_KEY, + regionUserSendKey => $OpenSim::Config::USER_SEND_KEY, + regionMapTextureID => $params->{"map-image-id"}, + serverHttpPort => $params->{http_port}, + serverRemotingPort => $params->{remoting_port}, + ); + eval { + &OpenSim::GridServer::GridManager::addRegion(\%new_region_data); + }; + if ($@) { + $response{"error"} = "unable to add region"; + return \%response; + } + $region_data = \%new_region_data; + } + + my @region_neighbours_data = (); + my $region_list = &OpenSim::GridServer::GridManager::getRegionList($region_data->{locX}-1, $region_data->{locY}-1, $region_data->{locX}+1, $region_data->{locY}+1); + foreach my $region (@$region_list) { + next if ($region->{regionHandle} eq $region_data->{regionHandle}); + my %neighbour_block = ( + "sim_ip" => $region->{serverIP}, + "sim_port" => $region->{serverPort}, + "region_locx" => $region->{locX}, + "region_locy" => $region->{locY}, + "UUID" => $region->{uuid}, + "regionHandle" => $region->{regionHandle}, + ); + push @region_neighbours_data, \%neighbour_block; + } + + %response = ( + UUID => $region_data->{uuid}, + region_locx => $region_data->{locX}, + region_locy => $region_data->{locY}, + regionname => $region_data->{regionName}, + estate_id => "1", # TODO ??? + neighbours => \@region_neighbours_data, + sim_ip => $region_data->{serverIP}, + sim_port => $region_data->{serverPort}, + asset_url => $region_data->{regionAssetURI}, + asset_recvkey => $region_data->{regionAssetRecvKey}, + asset_sendkey => $region_data->{regionAssetSendKey}, + user_url => $region_data->{regionUserURI}, + user_recvkey => $region_data->{regionUserRecvKey}, + user_sendkey => $region_data->{regionUserSendKey}, + authkey => $region_data->{regionSecret}, + data_uri => $region_data->{regionDataURI}, + "allow_forceful_banlines" => "TRUE", + ); + + return \%response; +} + +sub _simulator_data_request { + my $params = shift; + + my $region_data = undef; + my %response = (); + if ($params->{"region_UUID"}) { + $region_data = &OpenSim::GridServer::GridManager::getRegionByUUID($params->{"region_UUID"}); + } elsif ($params->{"region_handle"}) { + $region_data = &OpenSim::GridServer::GridManager::getRegionByHandle($params->{"region_handle"}); + } + if (!$region_data) { + $response{"error"} = "Sim does not exist"; + return \%response; + } + + $response{"sim_ip"} = $region_data->{serverIP}; + $response{"sim_port"} = $region_data->{serverPort}; + $response{"http_port"} = $region_data->{serverHttpPort}; + $response{"remoting_port"} = $region_data->{serverRemotingPort}; + $response{"region_locx"} = $region_data->{locX}; + $response{"region_locy"} = $region_data->{locY}; + $response{"region_UUID"} = $region_data->{uuid}; + $response{"region_name"} = $region_data->{regionName}; + $response{"regionHandle"} = $region_data->{regionHandle}; + + return \%response; +} + +sub _map_block { + my $params = shift; + + my $xmin = $params->{xmin} || 980; + my $ymin = $params->{ymin} || 980; + my $xmax = $params->{xmax} || 1020; + my $ymax = $params->{ymax} || 1020; + + my @sim_block_list = (); + my $region_list = &OpenSim::GridServer::GridManager::getRegionList($xmin, $ymin, $xmax, $ymax); + foreach my $region (@$region_list) { + my %sim_block = ( + "x" => $region->{locX}, + "y" => $region->{locY}, + "name" => $region->{regionName}, + "access" => 0, # TODO ? meaning unknown + "region-flags" => 0, # TODO ? unknown + "water-height" => 20, # TODO ? get from a XML + "agents" => 1, # TODO + "map-image-id" => $region->{regionMapTexture}, + "regionhandle" => $region->{regionHandle}, + "sim_ip" => $region->{serverIP}, + "sim_port" => $region->{serverPort}, + "sim_uri" => $region->{serverURI}, + "uuid" => $region->{uuid}, + "remoting_port" => $region->{serverRemotingPort}, + ); + push @sim_block_list, \%sim_block; + } + + my %response = ( + "sim-profiles" => \@sim_block_list, + ); + return \%response; +} + +sub _map_block2 { + my $params = shift; + + my $xmin = $params->{xmin} || 980; + my $ymin = $params->{ymin} || 980; + my $xmax = $params->{xmax} || 1020; + my $ymax = $params->{ymax} || 1020; + + my @sim_block_list = (); + my $region_list = &OpenSim::GridServer::GridManager::getRegionList2($xmin, $ymin, $xmax, $ymax); + foreach my $region (@$region_list) { + my %sim_block = ( + "x" => $region->{locX}, + "y" => $region->{locY}, + "name" => $region->{regionName}, + "access" => 0, # TODO ? meaning unknown + "region-flags" => 0, # TODO ? unknown + "water-height" => 20, # TODO ? get from a XML + "agents" => 1, # TODO + "map-image-id" => $region->{regionMapTexture}, + "regionhandle" => $region->{regionHandle}, + "sim_ip" => $region->{serverIP}, + "sim_port" => $region->{serverPort}, + "sim_uri" => $region->{serverURI}, + "uuid" => $region->{uuid}, + "remoting_port" => $region->{serverRemotingPort}, + ); + push @sim_block_list, \%sim_block; + } + + my %response = ( + "sim-profiles" => \@sim_block_list, + ); + return \%response; +} + +1; + diff --git a/share/perl/lib/OpenSim/GridServer/Config.pm b/share/perl/lib/OpenSim/GridServer/Config.pm new file mode 100644 index 0000000..dc72e5a --- /dev/null +++ b/share/perl/lib/OpenSim/GridServer/Config.pm @@ -0,0 +1,50 @@ +package OpenSim::GridServer::Config; + +use strict; + +our %SYS_SQL = ( + select_region_by_uuid => + "SELECT * FROM regions WHERE uuid=?", + select_region_by_handle => + "SELECT * FROM regions WHERE regionHandle=?", + select_region_list => + "SELECT * FROM regions WHERE locX>=? AND locX=? AND locY + "SELECT * FROM regions WHERE locX>=? AND locX=? AND locY + "INSERT INTO regions VALUES (?????????)", + delete_all_regions => + "delete from regions", +); + + +our @REGIONS_COLUMNS = ( + "uuid", + "regionHandle", + "regionName", + "regionRecvKey", + "regionSendKey", + "regionSecret", + "regionDataURI", + "serverIP", + "serverPort", + "serverURI", + "locX", + "locY", + "locZ", + "eastOverrideHandle", + "westOverrideHandle", + "southOverrideHandle", + "northOverrideHandle", + "regionAssetURI", + "regionAssetRecvKey", + "regionAssetSendKey", + "regionUserURI", + "regionUserRecvKey", + "regionUserSendKey", + "regionMapTexture", + "serverHttpPort", + "serverRemotingPort", +); + +1; diff --git a/share/perl/lib/OpenSim/GridServer/GridManager.pm b/share/perl/lib/OpenSim/GridServer/GridManager.pm new file mode 100644 index 0000000..2170d74 --- /dev/null +++ b/share/perl/lib/OpenSim/GridServer/GridManager.pm @@ -0,0 +1,57 @@ +package OpenSim::GridServer::GridManager; + +use strict; +use Carp; +use OpenSim::Utility; +use OpenSim::GridServer::Config; + +sub getRegionByUUID { + my $uuid = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::GridServer::Config::SYS_SQL{select_region_by_uuid}, $uuid); + my $count = @$result; + if ($count > 0) { + return $result->[0]; + } + Carp::croak("can not find region"); +} + +sub getRegionByHandle { + my $handle = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::GridServer::Config::SYS_SQL{select_region_by_handle}, $handle); + my $count = @$result; + if ($count > 0) { + return $result->[0]; + } + Carp::croak("can not find region # $handle"); +} + +sub getRegionList { + my ($xmin, $ymin, $xmax, $ymax) = @_; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::GridServer::Config::SYS_SQL{select_region_list}, $xmin, $xmax, $ymin, $ymax); + my $count = @$result; + if ($count > 0) { + return $result; + } + Carp::croak("can not find region"); +} + +sub getRegionList2 { + my ($xmin, $ymin, $xmax, $ymax) = @_; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::GridServer::Config::SYS_SQL{select_region_list2}, $xmin, $xmax, $ymin, $ymax); + my $count = @$result; + if ($count > 0) { + return $result; + } + Carp::croak("can not find region"); +} + +sub deleteRegions { + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::GridServer::Config::SYS_SQL{delete_all_regions}); + my $count = @$result; + if ($count > 0) { + return $result; + } + Carp::croak("failed to delete regions"); +} + +1; diff --git a/share/perl/lib/OpenSim/InventoryServer.pm b/share/perl/lib/OpenSim/InventoryServer.pm new file mode 100644 index 0000000..184e19a --- /dev/null +++ b/share/perl/lib/OpenSim/InventoryServer.pm @@ -0,0 +1,249 @@ +package OpenSim::InventoryServer; + +use strict; +use XML::Serializer; +use OpenSim::Utility; +use OpenSim::Config; +use OpenSim::InventoryServer::Config; +use OpenSim::InventoryServer::InventoryManager; + +my $METHOD_LIST = undef; + +sub getHandlerList { + if (!$METHOD_LIST) { + my %list = ( + "GetInventory" => \&_get_inventory, + "CreateInventory" => \&_create_inventory, + "NewFolder" => \&_new_folder, + "MoveFolder" => \&_move_folder, + "NewItem" => \&_new_item, + "DeleteItem" => \&_delete_item, + "RootFolders" => \&_root_folders, + ); + $METHOD_LIST = \%list; + } + return $METHOD_LIST; +} + +# ################# +# Handlers +sub _get_inventory { + my $post_data = shift; + my $uuid = &_get_uuid($post_data); + my $inventry_folders = &OpenSim::InventoryServer::InventoryManager::getUserInventoryFolders($uuid); + my @response_folders = (); + foreach (@$inventry_folders) { + my $folder = &_convert_to_response_folder($_); + push @response_folders, $folder; + } + my $inventry_items = &OpenSim::InventoryServer::InventoryManager::getUserInventoryItems($uuid); + my @response_items = (); + foreach (@$inventry_items) { + my $item = &_convert_to_response_item($_); + push @response_items, $item; + } + my $response_obj = { + Folders => { InventoryFolderBase => \@response_folders }, + AllItems => { InventoryItemBase => \@response_items }, + UserID => { UUID => $uuid }, + }; + my $serializer = new XML::Serializer( $response_obj, "InventoryCollection"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _create_inventory { + my $post_data = shift; + my $uuid = &_get_uuid($post_data); + my $InventoryFolders = &_create_default_inventory($uuid); + foreach (@$InventoryFolders) { + &OpenSim::InventoryServer::InventoryManager::saveInventoryFolder($_); + } + my $serializer = new XML::Serializer("true", "boolean"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _new_folder { + my $post_data = shift; + my $request_obj = &OpenSim::Utility::XML2Obj($post_data); + my $folder = &_convert_to_db_folder($request_obj); + &OpenSim::InventoryServer::InventoryManager::saveInventoryFolder($folder); + my $serializer = new XML::Serializer("true", "boolean"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _move_folder { + my $post_data = shift; + my $request_info = &OpenSim::Utility::XML2Obj($post_data); + &OpenSim::InventoryServer::InventoryManager::moveInventoryFolder($request_info); + my $serializer = new XML::Serializer("true", "boolean"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _new_item { + my $post_data = shift; + my $request_obj = &OpenSim::Utility::XML2Obj($post_data); + my $item = &_convert_to_db_item($request_obj); + &OpenSim::InventoryServer::InventoryManager::saveInventoryItem($item); + my $serializer = new XML::Serializer("true", "boolean"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _delete_item { + my $post_data = shift; + my $request_obj = &OpenSim::Utility::XML2Obj($post_data); + my $item_id = $request_obj->{inventoryID}->{UUID}; + &OpenSim::InventoryServer::InventoryManager::deleteInventoryItem($item_id); + my $serializer = new XML::Serializer("true", "boolean"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +sub _root_folders { + my $post_data = shift; + my $uuid = &_get_uuid($post_data); + my $response = undef; + my $inventory_root_folder = &OpenSim::InventoryServer::InventoryManager::getRootFolder($uuid); + if ($inventory_root_folder) { + my $root_folder_id = $inventory_root_folder->{folderID}; + my $root_folder = &_convert_to_response_folder($inventory_root_folder); + my $root_folders = &OpenSim::InventoryServer::InventoryManager::getChildrenFolders($root_folder_id); + my @folders = ($root_folder); + foreach(@$root_folders) { + my $folder = &_convert_to_response_folder($_); + push @folders, $folder; + } + $response = { InventoryFolderBase => \@folders }; + } else { + $response = ""; # TODO: need better failed message + } + my $serializer = new XML::Serializer($response, "ArrayOfInventoryFolderBase"); + return $serializer->to_formatted(XML::Serializer::WITH_HEADER); # TODO: +} + +# ################# +# subfunctions +sub _convert_to_db_item { + my $item = shift; + my $ret = { + inventoryID => $item->{inventoryID}->{UUID}, + assetID => $item->{assetID}->{UUID}, + assetType => $item->{assetType}, + invType => $item->{invType}, + parentFolderID => $item->{parentFolderID}->{UUID}, + avatarID => $item->{avatarID}->{UUID}, + creatorID => $item->{creatorsID}->{UUID}, # TODO: human error ??? + inventoryName => $item->{inventoryName}, + inventoryDescription => $item->{inventoryDescription} || "", + inventoryNextPermissions => $item->{inventoryNextPermissions}, + inventoryCurrentPermissions => $item->{inventoryCurrentPermissions}, + inventoryBasePermissions => $item->{inventoryBasePermissions}, + inventoryEveryOnePermissions => $item->{inventoryEveryOnePermissions}, + }; + return $ret; +} + +sub _convert_to_response_item { + my $item = shift; + my $ret = { + inventoryID => { UUID => $item->{inventoryID} }, + assetID => { UUID => $item->{assetID} }, + assetType => $item->{assetType}, + invType => $item->{invType}, + parentFolderID => { UUID => $item->{parentFolderID} }, + avatarID => { UUID => $item->{avatarID} }, + creatorsID => { UUID => $item->{creatorID} }, # TODO: human error ??? + inventoryName => $item->{inventoryName}, + inventoryDescription => $item->{inventoryDescription} || "", + inventoryNextPermissions => $item->{inventoryNextPermissions}, + inventoryCurrentPermissions => $item->{inventoryCurrentPermissions}, + inventoryBasePermissions => $item->{inventoryBasePermissions}, + inventoryEveryOnePermissions => $item->{inventoryEveryOnePermissions}, + }; + return $ret; +} + +sub _convert_to_db_folder { + my $folder = shift; + my $ret = { + folderName => $folder->{name}, + agentID => $folder->{agentID}->{UUID}, + parentFolderID => $folder->{parentID}->{UUID}, + folderID => $folder->{folderID}->{UUID}, + type => $folder->{type}, + version => $folder->{version}, + }; + return $ret; +} + +sub _convert_to_response_folder { + my $folder = shift; + my $ret = { + name => $folder->{folderName}, + agentID => { UUID => $folder->{agentID} }, + parentID => { UUID => $folder->{parentFolderID} }, + folderID => { UUID => $folder->{folderID} }, + type => $folder->{type}, + version => $folder->{version}, + }; + return $ret; +} + +sub _create_default_inventory { + my $uuid = shift; + + my @InventoryFolders = (); + my $root_folder_id = &OpenSim::Utility::GenerateUUID(); + + push @InventoryFolders, { + "folderID" => $root_folder_id, + "agentID" => $uuid, + "parentFolderID" => &OpenSim::Utility::ZeroUUID(), + "folderName" => "My Inventory", + "type" => 8, + "version" => 1, + }; + + push @InventoryFolders, { + "folderID" => &OpenSim::Utility::GenerateUUID(), + "agentID" => $uuid, + "parentFolderID" => $root_folder_id, + "folderName" => "Textures", + "type" => 0, + "version" => 1, + }; + + push @InventoryFolders, { + "folderID" => &OpenSim::Utility::GenerateUUID(), + "agentID" => $uuid, + "parentFolderID" => $root_folder_id, + "folderName" => "Objects", + "type" => 6, + "version" => 1, + }; + + push @InventoryFolders, { + "folderID" => &OpenSim::Utility::GenerateUUID(), + "agentID" => $uuid, + "parentFolderID" => $root_folder_id, + "folderName" => "Clothes", + "type" => 5, + "version" => 1, + }; + + return \@InventoryFolders; +} + + +# ################# +# Utilities +sub _get_uuid { + my $data = shift; + if ($data =~ /([^<]+)<\/guid>/) { + return $1; + } else { + Carp::croak("can not find uuid: $data"); + } +} + + +1; + diff --git a/share/perl/lib/OpenSim/InventoryServer/Config.pm b/share/perl/lib/OpenSim/InventoryServer/Config.pm new file mode 100644 index 0000000..64dbdd1 --- /dev/null +++ b/share/perl/lib/OpenSim/InventoryServer/Config.pm @@ -0,0 +1,51 @@ +package OpenSim::InventoryServer::Config; + +use strict; + +our %SYS_SQL = ( + save_inventory_folder => + "REPLACE INTO inventoryfolders VALUES (?,?,?,?,?,?)", + save_inventory_item => + "REPLACE INTO inventoryitems VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + get_root_folder => + "SELECT * FROM inventoryfolders WHERE parentFolderID=? AND agentId=?", + get_children_folders => + "SELECT * FROM inventoryfolders WHERE parentFolderID=?", + get_user_inventory_folders => + "SELECT * FROM inventoryfolders WHERE agentID=?", + get_user_inventory_items => + "SELECT * FROM inventoryitems WHERE avatarID=?", + delete_inventory_item => + "DELETE FROM inventoryitems WHERE inventoryID=?", + move_inventory_folder => + "UPDATE inventoryfolders SET parentFolderID=? WHERE folderID=?", +); + + +our @INVENTORYFOLDERS_COLUMNS = ( + "folderID", + "agentID", + "parentFolderID", + "folderName", + "type", + "version", +); + +our @INVENTORYITEMS_COLUMNS = ( + "inventoryID", + "assetID", + "type", + "parentFolderID", + "avatarID", + "inventoryName", + "inventoryDescription", + "inventoryNextPermissions", + "inventoryCurrentPermissions", + "assetType", + "invType", + "creatorID", + "inventoryBasePermissions", + "inventoryEveryOnePermissions", +); + +1; diff --git a/share/perl/lib/OpenSim/InventoryServer/InventoryManager.pm b/share/perl/lib/OpenSim/InventoryServer/InventoryManager.pm new file mode 100644 index 0000000..97111b7 --- /dev/null +++ b/share/perl/lib/OpenSim/InventoryServer/InventoryManager.pm @@ -0,0 +1,86 @@ +package OpenSim::InventoryServer::InventoryManager; + +use strict; +use Carp; +use OpenSim::Utility; +use OpenSim::InventoryServer::Config; + +sub saveInventoryFolder { + my $folder = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{save_inventory_folder}, + $folder->{"folderID"}, + $folder->{"agentID"}, + $folder->{"parentFolderID"}, + $folder->{"folderName"}, + $folder->{"type"}, + $folder->{"version"}); +} + +sub saveInventoryItem { + my $item = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{save_inventory_item}, + $item->{"inventoryID"}, + $item->{"assetID"}, + $item->{"type"}, + $item->{"parentFolderID"}, + $item->{"avatarID"}, + $item->{"inventoryName"}, + $item->{"inventoryDescription"}, + $item->{"inventoryNextPermissions"}, + $item->{"inventoryCurrentPermissions"}, + $item->{"assetType"}, + $item->{"invType"}, + $item->{"creatorID"}, + $item->{"inventoryBasePermissions"}, + $item->{"inventoryEveryOnePermissions"}); +} + +sub getRootFolder { + my $agent_id = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{get_root_folder}, + &OpenSim::Utility::ZeroUUID(), + $agent_id); + my $count = @$result; + if ($count > 0) { + return $result->[0]; + } else { + return undef; + } +} + +sub getChildrenFolders { + my $parent_id = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{get_children_folders}, $parent_id); + return $result; +} + +sub getUserInventoryFolders { + my $agent_id = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{get_user_inventory_folders}, + $agent_id); + return $result; +} + +sub getUserInventoryItems { + my $agent_id = shift; + my $result = &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{get_user_inventory_items}, + $agent_id); + return $result; +} + +sub deleteInventoryItem { + my $item_id = shift; + &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{delete_inventory_item}, + $item_id); +} + +sub moveInventoryFolder { + my $info = shift; + &OpenSim::Utility::getSimpleResult($OpenSim::InventoryServer::Config::SYS_SQL{move_inventory_folder}, + $info->{parentID}->{UUID}, # TODO: not good + $info->{folderID}->{UUID}, # TODO: not good UUID should be extracted in the higher level + ); +} + +1; + diff --git a/share/perl/lib/OpenSim/UserServer.pm b/share/perl/lib/OpenSim/UserServer.pm new file mode 100644 index 0000000..77117e1 --- /dev/null +++ b/share/perl/lib/OpenSim/UserServer.pm @@ -0,0 +1,239 @@ +package OpenSim::UserServer; + +use strict; +use OpenSim::Config; +use OpenSim::UserServer::Config; +use OpenSim::UserServer::UserManager; + +sub getHandlerList { + my %list = ( + "login_to_simulator" => \&_login_to_simulator, + "get_user_by_name" => \&_get_user_by_name, + "get_user_by_uuid" => \&_get_user_by_uuid, + "get_avatar_picker_avatar" => \&_get_avatar_picker_avatar, + ); + return \%list; +} + +# ################# +# Handlers +sub _login_to_simulator { + my $params = shift; + # check params + if (!$params->{first} || !$params->{last} || !$params->{passwd}) { + return &_make_false_response("not enough params", $OpenSim::Config::SYS_MSG{FATAL}); + } + # select user (check passwd) + my $user = &OpenSim::UserServer::UserManager::getUserByName($params->{first}, $params->{last}); + if ($user->{passwordHash} ne $params->{passwd}) { + &_make_false_response("password not match", $OpenSim::Config::SYS_MSG{FAIL}); + } + + # contact with Grid server + my %grid_request_params = ( + region_handle => $user->{homeRegion}, + authkey => undef + ); + my $grid_response = &OpenSim::Utility::XMLRPCCall($OpenSim::Config::GRID_SERVER_URL, "simulator_data_request", \%grid_request_params); + my $region_server_url = "http://" . $grid_response->{sim_ip} . ":" . $grid_response->{http_port}; + + # contact with Region server + my $session_id = &OpenSim::Utility::GenerateUUID; + my $secure_session_id = &OpenSim::Utility::GenerateUUID; + my $circuit_code = int(rand() * 1000000000); # just a random integer + my $caps_id = &OpenSim::Utility::GenerateUUID; + my %region_request_params = ( + session_id => $session_id, + secure_session_id => $secure_session_id, + firstname => $user->{username}, + lastname => $user->{lastname}, + agent_id => $user->{UUID}, + circuit_code => $circuit_code, + startpos_x => $user->{homeLocationX}, + startpos_y => $user->{homeLocationY}, + startpos_z => $user->{homeLocationZ}, + regionhandle => $user->{homeRegion}, + caps_path => $caps_id, + ); + my $region_response = &OpenSim::Utility::XMLRPCCall($region_server_url, "expect_user", \%region_request_params); + + # contact with Inventory server + my $inventory_data = &_create_inventory_data($user->{UUID}); + + # return to client + my %response = ( + # login info + login => "true", + session_id => $session_id, + secure_session_id => $secure_session_id, + # agent + first_name => $user->{username}, + last_name => $user->{lastname}, + agent_id => $user->{UUID}, + agent_access => "M", # TODO: do not know its meaning, hard coding in opensim + # grid + start_location => $params->{start}, + sim_ip => $grid_response->{sim_ip}, + sim_port => $grid_response->{sim_port}, + #sim_port => 9001, + region_x => $grid_response->{region_locx} * 256, + region_y => $grid_response->{region_locy} * 256, + # other + inventory_host => undef, # inv13-mysql + circuit_code => $circuit_code, + message => $OpenSim::Config::SYS_MSG{LOGIN_WELCOME}, + seconds_since_epoch => time, + seed_capability => $region_server_url . "/CAPS/" . $caps_id . "0000/", # https://sim2734.agni.lindenlab.com:12043/cap/61d6d8a0-2098-7eb4-2989-76265d80e9b6 + look_at => &_make_r_string($user->{homeLookAtX}, $user->{homeLookAtY}, $user->{homeLookAtZ}), + home => &_make_home_string( + [$grid_response->{region_locx} * 256, $grid_response->{region_locy} * 256], + [$user->{homeLocationX}, $user->{homeLocationY}, $user->{homeLocationX}], + [$user->{homeLookAtX}, $user->{homeLookAtY}, $user->{homeLookAtZ}]), + "inventory-skeleton" => $inventory_data->{InventoryArray}, + "inventory-root" => [ { folder_id => $inventory_data->{RootFolderID} } ], + "event_notifications" => \@OpenSim::UserServer::Config::event_notifications, + "event_categories" => \@OpenSim::UserServer::Config::event_categories, + "global-textures" => \@OpenSim::UserServer::Config::global_textures, + "inventory-lib-owner" => \@OpenSim::UserServer::Config::inventory_lib_owner, + "inventory-skel-lib" => \@OpenSim::UserServer::Config::inventory_skel_lib, # hard coding in OpenSim + "inventory-lib-root" => \@OpenSim::UserServer::Config::inventory_lib_root, + "classified_categories" => \@OpenSim::UserServer::Config::classified_categories, + "login-flags" => \@OpenSim::UserServer::Config::login_flags, + "initial-outfit" => \@OpenSim::UserServer::Config::initial_outfit, + "gestures" => \@OpenSim::UserServer::Config::gestures, + "ui-config" => \@OpenSim::UserServer::Config::ui_config, + ); + return \%response; +} + +sub _get_user_by_name { + my $param = shift; + + if ($param->{avatar_name}) { + my ($first, $last) = split(/\s+/, $param->{avatar_name}); + my $user = &OpenSim::UserServer::UserManager::getUserByName($first, $last); + if (!$user) { + return &_unknown_user_response; + } + return &_convert_to_response($user); + } else { + return &_unknown_user_response; + } +} + +sub _get_user_by_uuid { + my $param = shift; + + if ($param->{avatar_uuid}) { + my $user = &OpenSim::UserServer::UserManager::getUserByUUID($param->{avatar_uuid}); + if (!$user) { + return &_unknown_user_response; + } + return &_convert_to_response($user); + } else { + return &_unknown_user_response; + } +} + +sub _get_avatar_picker_avatar { +} + +# ################# +# sub functions +sub _create_inventory_data { + my $user_id = shift; + # TODO : too bad!! -> URI encoding + my $postdata =<< "POSTDATA"; +POSTDATA=$user_id +POSTDATA + my $res = &OpenSim::Utility::HttpPostRequest($OpenSim::Config::INVENTORY_SERVER_URL . "/RootFolders/", $postdata); + my $res_obj = &OpenSim::Utility::XML2Obj($res); + if (!$res_obj->{InventoryFolderBase}) { + &OpenSim::Utility::HttpPostRequest($OpenSim::Config::INVENTORY_SERVER_URL . "/CreateInventory/", $postdata); + # Sleep(10000); # TODO: need not to do this + $res = &OpenSim::Utility::HttpPostRequest($OpenSim::Config::INVENTORY_SERVER_URL . "/RootFolders/", $postdata); + $res_obj = &OpenSim::Utility::XML2Obj($res); + } + my $folders = $res_obj->{InventoryFolderBase}; + my $folders_count = @$folders; + if ($folders_count > 0) { + my @AgentInventoryFolders = (); + my $root_uuid = &OpenSim::Utility::ZeroUUID(); + foreach my $folder (@$folders) { + if ($folder->{parentID}->{UUID} eq &OpenSim::Utility::ZeroUUID()) { + $root_uuid = $folder->{folderID}->{UUID}; + } + my %folder_hash = ( + name => $folder->{name}, + parent_id => $folder->{parentID}->{UUID}, + version => $folder->{version}, + type_default => $folder->{type}, + folder_id => $folder->{folderID}->{UUID}, + ); + push @AgentInventoryFolders, \%folder_hash; + } + return { InventoryArray => \@AgentInventoryFolders, RootFolderID => $root_uuid }; + } else { + # TODO: impossible ??? + } + return undef; +} + +sub _convert_to_response { + my $user = shift; + my %response = ( + firstname => $user->{username}, + lastname => $user->{lastname}, + uuid => $user->{UUID}, + server_inventory => $user->{userInventoryURI}, + server_asset => $user->{userAssetURI}, + profile_about => $user->{profileAboutText}, + profile_firstlife_about => $user->{profileFirstText}, + profile_firstlife_image => $user->{profileFirstImage}, + profile_can_do => $user->{profileCanDoMask} || "0", + profile_want_do => $user->{profileWantDoMask} || "0", + profile_image => $user->{profileImage}, + profile_created => $user->{created}, + profile_lastlogin => $user->{lastLogin} || "0", + home_coordinates_x => $user->{homeLocationX}, + home_coordinates_y => $user->{homeLocationY}, + home_coordinates_z => $user->{homeLocationZ}, + home_region => $user->{homeRegion} || 0, + home_look_x => $user->{homeLookAtX}, + home_look_y => $user->{homeLookAtY}, + home_look_z => $user->{homeLookAtZ}, + ); + return \%response; +} + +# ################# +# Utility Functions +sub _make_false_response { + my ($reason, $message) = @_; + return { reason => $reason, login => "false", message => $message }; +} + +sub _unknown_user_response { + return { + error_type => "unknown_user", + error_desc => "The user requested is not in the database", + }; +} + +sub _make_home_string { + my ($region_handle, $position, $look_at) = @_; + my $region_handle_string = "'region_handle':" . &_make_r_string(@$region_handle); + my $position_string = "'position':" . &_make_r_string(@$position); + my $look_at_string = "'look_at':" . &_make_r_string(@$look_at); + return "{" . $region_handle_string . ", " . $position_string . ", " . $look_at_string . "}"; +} + +sub _make_r_string { + my @params = @_; + foreach (@params) { + $_ = "r" . $_; + } + return "[" . join(",", @params) . "]"; +} + +1; diff --git a/share/perl/lib/OpenSim/UserServer/Config.pm b/share/perl/lib/OpenSim/UserServer/Config.pm new file mode 100644 index 0000000..da628ed --- /dev/null +++ b/share/perl/lib/OpenSim/UserServer/Config.pm @@ -0,0 +1,125 @@ +package OpenSim::UserServer::Config; + +use strict; + +our %SYS_SQL = ( + select_user_by_name => + "select * from users where username=? and lastname=?", + select_user_by_uuid => + "select * from users where uuid=?", + create_user => + "insert into users values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", +); + +our @USERS_COLUMNS = ( + "UUID", + "username", + "lastname", + "passwordHash", + "passwordSalt", + "homeRegion", + "homeLocationX", + "homeLocationY", + "homeLocationZ", + "homeLookAtX", + "homeLookAtY", + "homeLookAtZ", + "created", + "lastLogin", + "userInventoryURI", + "userAssetURI", + "profileCanDoMask", + "profileWantDoMask", + "profileAboutText", + "profileFirstText", + "profileImage", + "profileFirstImage", +); + +# copied from opensim +our @classified_categories = ( + { category_id => 1, category_name => "Shopping" }, + { category_id => 2, category_name => "Land Rental" }, + { category_id => 3, category_name => "Property Rental" }, + { category_id => 4, category_name => "Special Attraction" }, + { category_id => 5, category_name => "New Products" }, + { category_id => 6, category_name => "Employment" }, + { category_id => 7, category_name => "Wanted" }, + { category_id => 8, category_name => "Service" }, + { category_id => 9, category_name => "Personal" }, +); + +our @event_categories = (); +our @event_notifications = (); +our @gestures =(); +our @global_textures = ( + { + cloud_texture_id => "dc4b9f0b-d008-45c6-96a4-01dd947ac621", + moon_texture_id => "ec4b9f0b-d008-45c6-96a4-01dd947ac621", + sun_texture_id => "cce0f112-878f-4586-a2e2-a8f104bba271", + }, +); +our @initial_outfit = ( + { folder_name => "Nightclub Female", gender => "female" } +); +our @inventory_lib_owner = ({ agent_id => "11111111-1111-0000-0000-000100bba000" }); +our @inventory_lib_root = ({ folder_id => "00000112-000f-0000-0000-000100bba000" }); +our @inventory_root = ({ folder_id => "2eb27bc2-22ee-48db-b2e9-5c79a6582919" }); +our @inventory_skel_lib = ( + { + folder_id => "00000112-000f-0000-0000-000100bba000", + name => "OpenSim Library", + parent_id => "00000000-0000-0000-0000-000000000000", + type_default => -1, + version => 1, + }, + { + folder_id => "00000112-000f-0000-0000-000100bba001", + name => "Texture Library", + parent_id => "00000112-000f-0000-0000-000100bba000", + type_default => -1, + version => 1, + }, +); +our @inventory_skeleton = ( + { + folder_id => "2eb27bc2-22ee-48db-b2e9-5c79a6582919", + name => "My Inventory", + parent_id => "00000000-0000-0000-0000-000000000000", + type_default => 8, + version => 1, + }, + { + folder_id => "6cc20d86-9945-4997-a102-959348d56821", + name => "Textures", + parent_id => "2eb27bc2-22ee-48db-b2e9-5c79a6582919", + type_default => 0, + version => 1, + }, + { + folder_id => "840b747f-bb7d-465e-ab5a-58badc953484", + name => "Clothes", + parent_id => "2eb27bc2-22ee-48db-b2e9-5c79a6582919", + type_default => 5, + version => 1, + }, + { + folder_id => "37039005-7bbe-42a2-aa12-6bda453f37fd", + name => "Objects", + parent_id => "2eb27bc2-22ee-48db-b2e9-5c79a6582919", + type_default => 6, + version => 1, + }, +); +our @login_flags = ( + { + daylight_savings => "N", + ever_logged_in => "Y", + gendered => "Y", + stipend_since_login => "N", + }, +); +our @ui_config = ({ allow_first_life => "Y" }); + +1; + diff --git a/share/perl/lib/OpenSim/UserServer/UserManager.pm b/share/perl/lib/OpenSim/UserServer/UserManager.pm new file mode 100644 index 0000000..ce35329 --- /dev/null +++ b/share/perl/lib/OpenSim/UserServer/UserManager.pm @@ -0,0 +1,49 @@ +package OpenSim::UserServer::UserManager; + +use strict; +use Carp; +use OpenSim::Utility; +use OpenSim::UserServer::Config; + +sub getUserByName { + my ($first, $last) = @_; + my $res = &OpenSim::Utility::getSimpleResult($OpenSim::UserServer::Config::SYS_SQL{select_user_by_name}, $first, $last); + my $count = @$res; + my %user = (); + if ($count == 1) { + my $user_row = $res->[0]; + foreach (@OpenSim::UserServer::Config::USERS_COLUMNS) { + $user{$_} = $user_row->{$_} || ""; + } + } else { + Carp::croak("user not found"); + } + return \%user; +} + +sub getUserByUUID { + my ($uuid) = @_; + my $res = &OpenSim::Utility::getSimpleResult($OpenSim::UserServer::Config::SYS_SQL{select_user_by_uuid}, $uuid); + my $count = @$res; + my %user = (); + if ($count == 1) { + my $user_row = $res->[0]; + foreach (@OpenSim::UserServer::Config::USERS_COLUMNS) { + $user{$_} = $user_row->{$_} || ""; + } + } else { + Carp::croak("user not found"); + } + return \%user; +} + +sub createUser { + my $user = shift; + my @params = (); + foreach (@OpenSim::UserServer::Config::USERS_COLUMNS) { + push @params, $user->{$_}; + } + my $res = &OpenSim::Utility::getSimpleResult($OpenSim::UserServer::Config::SYS_SQL{create_user}, @params); +} + +1; diff --git a/share/perl/lib/OpenSim/Utility.pm b/share/perl/lib/OpenSim/Utility.pm new file mode 100644 index 0000000..7fc91e7 --- /dev/null +++ b/share/perl/lib/OpenSim/Utility.pm @@ -0,0 +1,155 @@ +package OpenSim::Utility; + +use strict; +use XML::RPC; +use XML::Simple; +use Data::UUID; +use DBHandler; +use OpenSim::Config; +use Socket; + +sub XMLRPCCall { + my ($url, $methodname, $param) = @_; + my $xmlrpc = new XML::RPC($url); + my $result = $xmlrpc->call($methodname, $param); + return $result; +} + +sub XMLRPCCall_array { + my ($url, $methodname, $param) = @_; + my $xmlrpc = new XML::RPC($url); + my $result = $xmlrpc->call($methodname, @$param); + return $result; +} + +sub UIntsToLong { + my ($int1, $int2) = @_; + return $int1 * 4294967296 + $int2; +} + +sub getSimpleResult { + my ($sql, @args) = @_; + my $dbh = &DBHandler::getConnection($OpenSim::Config::DSN, $OpenSim::Config::DBUSER, $OpenSim::Config::DBPASS); + my $st = new Statement($dbh, $sql); + return $st->exec(@args); +} + +sub GenerateUUID { + my $ug = new Data::UUID(); + my $uuid = $ug->create(); + return $ug->to_string($uuid); +} + +sub ZeroUUID { + return "00000000-0000-0000-0000-000000000000"; +} + +sub HEX2UUID { + my $hex = shift; + Carp::croak("$hex is not a uuid") if (length($hex) != 32); + my @sub_uuids = ($hex =~ /(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/); + return join("-", @sub_uuids); +} + +sub BIN2UUID { + # TODO: +} + +sub UUID2HEX { + my $uuid = shift; + $uuid =~ s/-//g; + return $uuid; +} + +sub UUID2BIN { + my $uuid = shift; + return pack("H*", &UUID2HEX($uuid)); +} + +sub HttpPostRequest { + my ($url, $postdata) = @_; + $url =~ /http:\/\/([^:\/]+)(:([0-9]+))?(\/.*)?/; + my ($host, $port, $path) = ($1, $3, $4); + $port ||= 80; + $path ||= "/"; + my $CRLF= "\015\012"; + my $addr = (gethostbyname($host))[4]; + my $name = pack('S n a4 x8', 2, $port, $addr); + my $data_len = length($postdata); + socket(SOCK, PF_INET, SOCK_STREAM, 0); + connect(SOCK, $name) ; + select(SOCK); $| = 1; select(STDOUT); + print SOCK "POST $path HTTP/1.0$CRLF"; + print SOCK "Host: $host:$port$CRLF"; + print SOCK "Content-Length: $data_len$CRLF"; + print SOCK "$CRLF"; + print SOCK $postdata; + + my $ret = ""; + unless () { + close(SOCK); + Carp::croak("can not connect to $url"); + } + my $header = ""; + while () { + $header .= $_; + last if ($_ eq $CRLF); + } + if ($header != /200/) { + return $ret; + } + while () { + $ret .= $_; + } + return $ret; +} +# TODO : merge with POST +sub HttpGetRequest { + my ($url) = @_; + $url =~ /http:\/\/([^:\/]+)(:([0-9]+))?(\/.*)?/; + my ($host, $port, $path) = ($1, $3, $4); + $port ||= 80; + $path ||= "/"; + my $CRLF= "\015\012"; + my $addr = (gethostbyname($host))[4]; + my $name = pack('S n a4 x8', 2, $port, $addr); + socket(SOCK, PF_INET, SOCK_STREAM, 0); + connect(SOCK, $name) ; + select(SOCK); $| = 1; select(STDOUT); + print SOCK "GET $path HTTP/1.0$CRLF"; + print SOCK "Host: $host:$port$CRLF"; + print SOCK "$CRLF"; + + unless () { + close(SOCK); + Carp::croak("can not connect to $url"); + } + while () { + last if ($_ eq $CRLF); + } + my $ret = ""; + while () { + $ret .= $_; + } + return $ret; +} + +sub XML2Obj { + my $xml = shift; + my $xs = new XML::Simple( keyattr=>[] ); + return $xs->XMLin($xml); +} + +sub Log { + my $server_name = shift; + my @param = @_; + open(FILE, ">>" . $OpenSim::Config::DEBUG_LOGDIR . "/" . $server_name . ".log"); + foreach(@param) { + print FILE $_ . "\n"; + } + print FILE "<<<<<<<<<<<=====================\n\n"; + close(FILE); +} + +1; + diff --git a/share/perl/lib/XML/RPC.pm b/share/perl/lib/XML/RPC.pm new file mode 100644 index 0000000..2e08867 --- /dev/null +++ b/share/perl/lib/XML/RPC.pm @@ -0,0 +1,217 @@ +package XML::RPC; + +use strict; +use XML::TreePP; +use Data::Dumper; +use vars qw($VERSION $faultCode); +no strict 'refs'; + +$VERSION = 0.5; + +sub new { + my $package = shift; + my $self = { }; + bless $self, $package; + $self->{url} = shift; + $self->{tpp} = XML::TreePP->new(@_); + return $self; +} + +sub call { + my $self = shift; + my ( $methodname, @params ) = @_; + + die 'no url' if ( !$self->{url} ); + + $faultCode = 0; + my $xml = $self->create_call_xml( $methodname, @params ); +#print STDERR $xml; + my $result = $self->{tpp}->parsehttp( + POST => $self->{url}, + $xml, + { + 'Content-Type' => 'text/xml', + 'User-Agent' => 'XML-RPC/' . $VERSION, + 'Content-Length' => length($xml) + } + ); + + my @data = $self->unparse_response($result); + return @data == 1 ? $data[0] : @data; +} + +sub receive { + my $self = shift; + my $result = eval { + my $xml = shift || die 'no xml'; + my $handler = shift || die 'no handler'; + my $hash = $self->{tpp}->parse($xml); + my ( $methodname, @params ) = $self->unparse_call($hash); + $self->create_response_xml( $handler->( $methodname, @params ) ); + }; + return $self->create_fault_xml($@) if ($@); + return $result; + +} + +sub create_fault_xml { + my $self = shift; + my $error = shift; + chomp($error); + return $self->{tpp} + ->write( { methodResponse => { fault => $self->parse( { faultString => $error, faultCode => $faultCode } ) } } ); +} + +sub create_call_xml { + my $self = shift; + my ( $methodname, @params ) = @_; + + return $self->{tpp}->write( + { + methodCall => { + methodName => $methodname, + params => { param => [ map { $self->parse($_) } @params ] } + } + } + ); +} + +sub create_response_xml { + my $self = shift; + my @params = @_; + + return $self->{tpp}->write( { methodResponse => { params => { param => [ map { $self->parse($_) } @params ] } } } ); +} + +sub parse { + my $self = shift; + my $p = shift; + my $result; + + if ( ref($p) eq 'HASH' ) { + $result = $self->parse_struct($p); + } + elsif ( ref($p) eq 'ARRAY' ) { + $result = $self->parse_array($p); + } + else { + $result = $self->parse_scalar($p); + } + + return { value => $result }; +} + +sub parse_scalar { + my $self = shift; + my $scalar = shift; + local $^W = undef; + + if ( ( $scalar =~ m/^[\-+]?\d+$/ ) + && ( abs($scalar) <= ( 0xffffffff >> 1 ) ) ) + { + return { i4 => $scalar }; + } + elsif ( $scalar =~ m/^[\-+]?\d+\.\d+$/ ) { + return { double => $scalar }; + } + else { + return { string => \$scalar }; + } +} + +sub parse_struct { + my $self = shift; + my $hash = shift; + my @members; + while ( my ( $k, $v ) = each(%$hash) ) { + push @members, { name => $k, %{ $self->parse($v) } }; + } + return { struct => { member => \@members } }; +} + +sub parse_array { + my $self = shift; + my $array = shift; + + return { array => { data => { value => [ map { $self->parse($_)->{value} } $self->list($array) ] } } }; +} + +sub unparse_response { + my $self = shift; + my $hash = shift; + + my $response = $hash->{methodResponse} || die 'no data'; + + if ( $response->{fault} ) { + return $self->unparse_value( $response->{fault}->{value} ); + } + else { + return map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} ); + } +} + +sub unparse_call { + my $self = shift; + my $hash = shift; + + my $response = $hash->{methodCall} || die 'no data'; + + my $methodname = $response->{methodName}; + my @args = + map { $self->unparse_value( $_->{value} ) } $self->list( $response->{params}->{param} ); + return ( $methodname, @args ); +} + +sub unparse_value { + my $self = shift; + my $value = shift; + my $result; + + return $value if ( ref($value) ne 'HASH' ); # for unspecified params + if ( $value->{struct} ) { + $result = $self->unparse_struct( $value->{struct} ); + return !%$result + ? undef + : $result; # fix for empty hashrefs from XML::TreePP + } + elsif ( $value->{array} ) { + return $self->unparse_array( $value->{array} ); + } + else { + return $self->unparse_scalar($value); + } +} + +sub unparse_scalar { + my $self = shift; + my $scalar = shift; + my ($result) = values(%$scalar); + return ( ref($result) eq 'HASH' && !%$result ) + ? undef + : $result; # fix for empty hashrefs from XML::TreePP +} + +sub unparse_struct { + my $self = shift; + my $struct = shift; + + return { map { $_->{name} => $self->unparse_value( $_->{value} ) } $self->list( $struct->{member} ) }; +} + +sub unparse_array { + my $self = shift; + my $array = shift; + my $data = $array->{data}; + + return [ map { $self->unparse_value($_) } $self->list( $data->{value} ) ]; +} + +sub list { + my $self = shift; + my $param = shift; + return () if ( !$param ); + return @$param if ( ref($param) eq 'ARRAY' ); + return ($param); +} + +1; diff --git a/share/perl/lib/XML/Serializer.pm b/share/perl/lib/XML/Serializer.pm new file mode 100644 index 0000000..6e64f17 --- /dev/null +++ b/share/perl/lib/XML/Serializer.pm @@ -0,0 +1,163 @@ +package XML::Serializer; + +use strict; + +my $root_element = "root"; +my $indent = " "; +#my $XML_HEADER = << "XMLHEADER"; +# +# +#XMLHEADER +my $XML_HEADER = << "XMLHEADER"; + +XMLHEADER + +sub WITH_HEADER { + return 1; +} + +sub new { + my ($this, $data, $root_name, $xslt) = @_; + my %fields = ( + _charset => "utf-8", + _data => "", + _output => "", + _root_name => $root_name ? $root_name : "root", + _xslt => $xslt ? $xslt : "" + ); + if (defined $data) { + $fields{_data} = $data; + } + return bless \%fields, $this; +} + +sub set_root_name { + my ($this, $root_name) = @_; + $this->{_root_name} = $root_name; +} + +sub set_data { + my ($this, $data) = @_; + $this->{_data} = $data; +} + +sub set_charset { + my ($this, $charset) = @_; + $this->{_charset} = $charset; +} + +sub set_xslt { + my ($this, $xslt) = @_; + $this->{_xslt} = $xslt; +} + +sub to_string{ + my ($this, $header) = @_; + if ($header) { + $this->{_output} = &_make_xml_header($this->{_charset}, $this->{_xslt}); + } + $this->{_output} .= &_to_string($this->{_data}, $this->{_root_name}); +} + +sub to_formatted{ + my ($this, $header) = @_; + if ($header) { + $this->{_output} = &_make_xml_header($this->{_charset}, $this->{_xslt}); + } + $this->{_output} .= &_to_formatted($this->{_root_name}, $this->{_data}); +} + +sub _make_xml_header { + my $header = $XML_HEADER; + $header =~ s/__CHARSET__/$_[0]/; + $header =~ s/__XSLT__/$_[1]/; + return $header; +} + +sub _to_string { + my ($obj, $name) = @_; + my $output = ""; + + if (ref($obj) eq "HASH") { + my $attr_list = ""; + my $tmp_mid = ""; + foreach (sort keys %$obj) { + if ($_ =~ /^@/) { + $attr_list = &_to_string($_, $obj->{$_}); + } + $tmp_mid .= &_to_string($_, $obj->{$_}); + } + $output = &_start_node($name, $attr_list) . $tmp_mid . &_end_node($name); + } + elsif (ref($obj) eq "ARRAY") { + foreach (@$obj) { + $output .= &_to_string($_, $name); + } + } + else { + if ($_ =~ /^@(.+)$/) { + return "$1=\"$obj\" "; + } else { + $output = &_start_node($name) . $obj . &_end_node($name); + } + } + return $output; +} + +sub _to_formatted { + my ($name, $obj, $depth) = @_; +# if (!$obj) { $obj = ""; } + if (!defined($depth)) { $depth = 0; } + my $output = ""; + if (ref($obj) eq "HASH") { + my $attr_list = ""; + my $tmp_mid = ""; + foreach (sort keys %$obj) { + if ($_ =~ /^@/) { + $attr_list = &_to_string($_, $obj->{$_}); + } + $tmp_mid .= &_to_formatted($_, $obj->{$_}, $depth+1); + } + $output = &_start_node($name, $attr_list, $depth) . "\n" . $tmp_mid . &_end_node($name, $depth); + } + elsif (ref($obj) eq "ARRAY") { + foreach (@$obj) { + $output .= &_to_formatted($name, $_, $depth); + } + } + else { + if ($_ =~ /^@(.+)$/) { + #return "$1=\"$obj\" "; + } else { + $output .= &_start_node($name, "", $depth); + $output .= $obj; + $output .= &_end_node($name); + } + } + return $output; +} + +sub _start_node { + my $ret = ""; + if (defined $_[2]) { + for(1..$_[2]) { $ret .= $indent; } + } + my $tag = $_[0] ? $_[0] : ""; + my $attr = $_[1] ? $_[1] : ""; + $ret .= "<$tag $attr>"; + return $ret; +} + +sub _end_node { + my $ret = ""; + if (defined $_[1]) { + for(1..$_[1]) { $ret .= $indent; } + } + if (defined $_[0]) { + $ret .= "\n"; + } + return $ret; +} + +1; + diff --git a/share/perl/lib/XML/Simple.pm b/share/perl/lib/XML/Simple.pm new file mode 100644 index 0000000..993669b --- /dev/null +++ b/share/perl/lib/XML/Simple.pm @@ -0,0 +1,3284 @@ +# $Id: Simple.pm,v 1.1 2008/01/18 09:10:19 ryu Exp $ + +package XML::Simple; + +=head1 NAME + +XML::Simple - Easy API to maintain XML (esp config files) + +=head1 SYNOPSIS + + use XML::Simple; + + my $ref = XMLin([] [, ]); + + my $xml = XMLout($hashref [, ]); + +Or the object oriented way: + + require XML::Simple; + + my $xs = XML::Simple->new(options); + + my $ref = $xs->XMLin([] [, ]); + + my $xml = $xs->XMLout($hashref [, ]); + +(or see L<"SAX SUPPORT"> for 'the SAX way'). + +To catch common errors: + + use XML::Simple qw(:strict); + +(see L<"STRICT MODE"> for more details). + +=cut + +# See after __END__ for more POD documentation + + +# Load essentials here, other modules loaded on demand later + +use strict; +use Carp; +require Exporter; + + +############################################################################## +# Define some constants +# + +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $PREFERRED_PARSER); + +@ISA = qw(Exporter); +@EXPORT = qw(XMLin XMLout); +@EXPORT_OK = qw(xml_in xml_out); +$VERSION = '2.18'; +$PREFERRED_PARSER = undef; + +my $StrictMode = 0; + +my @KnownOptIn = qw(keyattr keeproot forcecontent contentkey noattr + searchpath forcearray cache suppressempty parseropts + grouptags nsexpand datahandler varattr variables + normalisespace normalizespace valueattr); + +my @KnownOptOut = qw(keyattr keeproot contentkey noattr + rootname xmldecl outputfile noescape suppressempty + grouptags nsexpand handler noindent attrindent nosort + valueattr numericescape); + +my @DefKeyAttr = qw(name key id); +my $DefRootName = qq(opt); +my $DefContentKey = qq(content); +my $DefXmlDecl = qq(); + +my $xmlns_ns = 'http://www.w3.org/2000/xmlns/'; +my $bad_def_ns_jcn = '{' . $xmlns_ns . '}'; # LibXML::SAX workaround + + +############################################################################## +# Globals for use by caching routines +# + +my %MemShareCache = (); +my %MemCopyCache = (); + + +############################################################################## +# Wrapper for Exporter - handles ':strict' +# + +sub import { + # Handle the :strict tag + + $StrictMode = 1 if grep(/^:strict$/, @_); + + # Pass everything else to Exporter.pm + + @_ = grep(!/^:strict$/, @_); + goto &Exporter::import; +} + + +############################################################################## +# Constructor for optional object interface. +# + +sub new { + my $class = shift; + + if(@_ % 2) { + croak "Default options must be name=>value pairs (odd number supplied)"; + } + + my %known_opt; + @known_opt{@KnownOptIn, @KnownOptOut} = (undef) x 100; + + my %raw_opt = @_; + my %def_opt; + while(my($key, $val) = each %raw_opt) { + my $lkey = lc($key); + $lkey =~ s/_//g; + croak "Unrecognised option: $key" unless(exists($known_opt{$lkey})); + $def_opt{$lkey} = $val; + } + my $self = { def_opt => \%def_opt }; + + return(bless($self, $class)); +} + + +############################################################################## +# Sub: _get_object() +# +# Helper routine called from XMLin() and XMLout() to create an object if none +# was provided. Note, this routine does mess with the caller's @_ array. +# + +sub _get_object { + my $self; + if($_[0] and UNIVERSAL::isa($_[0], 'XML::Simple')) { + $self = shift; + } + else { + $self = XML::Simple->new(); + } + + return $self; +} + + +############################################################################## +# Sub/Method: XMLin() +# +# Exported routine for slurping XML into a hashref - see pod for info. +# +# May be called as object method or as a plain function. +# +# Expects one arg for the source XML, optionally followed by a number of +# name => value option pairs. +# + +sub XMLin { + my $self = &_get_object; # note, @_ is passed implicitly + + my $target = shift; + + + # Work out whether to parse a string, a file or a filehandle + + if(not defined $target) { + return $self->parse_file(undef, @_); + } + + elsif($target eq '-') { + local($/) = undef; + $target = ; + return $self->parse_string(\$target, @_); + } + + elsif(my $type = ref($target)) { + if($type eq 'SCALAR') { + return $self->parse_string($target, @_); + } + else { + return $self->parse_fh($target, @_); + } + } + + elsif($target =~ m{<.*?>}s) { + return $self->parse_string(\$target, @_); + } + + else { + return $self->parse_file($target, @_); + } +} + + +############################################################################## +# Sub/Method: parse_file() +# +# Same as XMLin, but only parses from a named file. +# + +sub parse_file { + my $self = &_get_object; # note, @_ is passed implicitly + + my $filename = shift; + + $self->handle_options('in', @_); + + $filename = $self->default_config_file if not defined $filename; + + $filename = $self->find_xml_file($filename, @{$self->{opt}->{searchpath}}); + + # Check cache for previous parse + + if($self->{opt}->{cache}) { + foreach my $scheme (@{$self->{opt}->{cache}}) { + my $method = 'cache_read_' . $scheme; + my $opt = $self->$method($filename); + return($opt) if($opt); + } + } + + my $ref = $self->build_simple_tree($filename, undef); + + if($self->{opt}->{cache}) { + my $method = 'cache_write_' . $self->{opt}->{cache}->[0]; + $self->$method($ref, $filename); + } + + return $ref; +} + + +############################################################################## +# Sub/Method: parse_fh() +# +# Same as XMLin, but only parses from a filehandle. +# + +sub parse_fh { + my $self = &_get_object; # note, @_ is passed implicitly + + my $fh = shift; + croak "Can't use " . (defined $fh ? qq{string ("$fh")} : 'undef') . + " as a filehandle" unless ref $fh; + + $self->handle_options('in', @_); + + return $self->build_simple_tree(undef, $fh); +} + + +############################################################################## +# Sub/Method: parse_string() +# +# Same as XMLin, but only parses from a string or a reference to a string. +# + +sub parse_string { + my $self = &_get_object; # note, @_ is passed implicitly + + my $string = shift; + + $self->handle_options('in', @_); + + return $self->build_simple_tree(undef, ref $string ? $string : \$string); +} + + +############################################################################## +# Method: default_config_file() +# +# Returns the name of the XML file to parse if no filename (or XML string) +# was provided. +# + +sub default_config_file { + my $self = shift; + + require File::Basename; + + my($basename, $script_dir, $ext) = File::Basename::fileparse($0, '\.[^\.]+'); + + # Add script directory to searchpath + + if($script_dir) { + unshift(@{$self->{opt}->{searchpath}}, $script_dir); + } + + return $basename . '.xml'; +} + + +############################################################################## +# Method: build_simple_tree() +# +# Builds a 'tree' data structure as provided by XML::Parser and then +# 'simplifies' it as specified by the various options in effect. +# + +sub build_simple_tree { + my $self = shift; + + my $tree = $self->build_tree(@_); + + return $self->{opt}->{keeproot} + ? $self->collapse({}, @$tree) + : $self->collapse(@{$tree->[1]}); +} + + +############################################################################## +# Method: build_tree() +# +# This routine will be called if there is no suitable pre-parsed tree in a +# cache. It parses the XML and returns an XML::Parser 'Tree' style data +# structure (summarised in the comments for the collapse() routine below). +# +# XML::Simple requires the services of another module that knows how to parse +# XML. If XML::SAX is installed, the default SAX parser will be used, +# otherwise XML::Parser will be used. +# +# This routine expects to be passed a filename as argument 1 or a 'string' as +# argument 2. The 'string' might be a string of XML (passed by reference to +# save memory) or it might be a reference to an IO::Handle. (This +# non-intuitive mess results in part from the way XML::Parser works but that's +# really no excuse). +# + +sub build_tree { + my $self = shift; + my $filename = shift; + my $string = shift; + + + my $preferred_parser = $PREFERRED_PARSER; + unless(defined($preferred_parser)) { + $preferred_parser = $ENV{XML_SIMPLE_PREFERRED_PARSER} || ''; + } + if($preferred_parser eq 'XML::Parser') { + return($self->build_tree_xml_parser($filename, $string)); + } + + eval { require XML::SAX; }; # We didn't need it until now + if($@) { # No XML::SAX - fall back to XML::Parser + if($preferred_parser) { # unless a SAX parser was expressly requested + croak "XMLin() could not load XML::SAX"; + } + return($self->build_tree_xml_parser($filename, $string)); + } + + $XML::SAX::ParserPackage = $preferred_parser if($preferred_parser); + + my $sp = XML::SAX::ParserFactory->parser(Handler => $self); + + $self->{nocollapse} = 1; + my($tree); + if($filename) { + $tree = $sp->parse_uri($filename); + } + else { + if(ref($string) && ref($string) ne 'SCALAR') { + $tree = $sp->parse_file($string); + } + else { + $tree = $sp->parse_string($$string); + } + } + + return($tree); +} + + +############################################################################## +# Method: build_tree_xml_parser() +# +# This routine will be called if XML::SAX is not installed, or if XML::Parser +# was specifically requested. It takes the same arguments as build_tree() and +# returns the same data structure (XML::Parser 'Tree' style). +# + +sub build_tree_xml_parser { + my $self = shift; + my $filename = shift; + my $string = shift; + + + eval { + local($^W) = 0; # Suppress warning from Expat.pm re File::Spec::load() + require XML::Parser; # We didn't need it until now + }; + if($@) { + croak "XMLin() requires either XML::SAX or XML::Parser"; + } + + if($self->{opt}->{nsexpand}) { + carp "'nsexpand' option requires XML::SAX"; + } + + my $xp = XML::Parser->new(Style => 'Tree', @{$self->{opt}->{parseropts}}); + my($tree); + if($filename) { + # $tree = $xp->parsefile($filename); # Changed due to prob w/mod_perl + local(*XML_FILE); + open(XML_FILE, '<', $filename) || croak qq($filename - $!); + $tree = $xp->parse(*XML_FILE); + close(XML_FILE); + } + else { + $tree = $xp->parse($$string); + } + + return($tree); +} + + +############################################################################## +# Method: cache_write_storable() +# +# Wrapper routine for invoking Storable::nstore() to cache a parsed data +# structure. +# + +sub cache_write_storable { + my($self, $data, $filename) = @_; + + my $cachefile = $self->storable_filename($filename); + + require Storable; # We didn't need it until now + + if ('VMS' eq $^O) { + Storable::nstore($data, $cachefile); + } + else { + # If the following line fails for you, your Storable.pm is old - upgrade + Storable::lock_nstore($data, $cachefile); + } + +} + + +############################################################################## +# Method: cache_read_storable() +# +# Wrapper routine for invoking Storable::retrieve() to read a cached parsed +# data structure. Only returns cached data if the cache file exists and is +# newer than the source XML file. +# + +sub cache_read_storable { + my($self, $filename) = @_; + + my $cachefile = $self->storable_filename($filename); + + return unless(-r $cachefile); + return unless((stat($cachefile))[9] > (stat($filename))[9]); + + require Storable; # We didn't need it until now + + if ('VMS' eq $^O) { + return(Storable::retrieve($cachefile)); + } + else { + return(Storable::lock_retrieve($cachefile)); + } + +} + + +############################################################################## +# Method: storable_filename() +# +# Translates the supplied source XML filename into a filename for the storable +# cached data. A '.stor' suffix is added after stripping an optional '.xml' +# suffix. +# + +sub storable_filename { + my($self, $cachefile) = @_; + + $cachefile =~ s{(\.xml)?$}{.stor}; + return $cachefile; +} + + +############################################################################## +# Method: cache_write_memshare() +# +# Takes the supplied data structure reference and stores it away in a global +# hash structure. +# + +sub cache_write_memshare { + my($self, $data, $filename) = @_; + + $MemShareCache{$filename} = [time(), $data]; +} + + +############################################################################## +# Method: cache_read_memshare() +# +# Takes a filename and looks in a global hash for a cached parsed version. +# + +sub cache_read_memshare { + my($self, $filename) = @_; + + return unless($MemShareCache{$filename}); + return unless($MemShareCache{$filename}->[0] > (stat($filename))[9]); + + return($MemShareCache{$filename}->[1]); + +} + + +############################################################################## +# Method: cache_write_memcopy() +# +# Takes the supplied data structure and stores a copy of it in a global hash +# structure. +# + +sub cache_write_memcopy { + my($self, $data, $filename) = @_; + + require Storable; # We didn't need it until now + + $MemCopyCache{$filename} = [time(), Storable::dclone($data)]; +} + + +############################################################################## +# Method: cache_read_memcopy() +# +# Takes a filename and looks in a global hash for a cached parsed version. +# Returns a reference to a copy of that data structure. +# + +sub cache_read_memcopy { + my($self, $filename) = @_; + + return unless($MemCopyCache{$filename}); + return unless($MemCopyCache{$filename}->[0] > (stat($filename))[9]); + + return(Storable::dclone($MemCopyCache{$filename}->[1])); + +} + + +############################################################################## +# Sub/Method: XMLout() +# +# Exported routine for 'unslurping' a data structure out to XML. +# +# Expects a reference to a data structure and an optional list of option +# name => value pairs. +# + +sub XMLout { + my $self = &_get_object; # note, @_ is passed implicitly + + croak "XMLout() requires at least one argument" unless(@_); + my $ref = shift; + + $self->handle_options('out', @_); + + + # If namespace expansion is set, XML::NamespaceSupport is required + + if($self->{opt}->{nsexpand}) { + require XML::NamespaceSupport; + $self->{nsup} = XML::NamespaceSupport->new(); + $self->{ns_prefix} = 'aaa'; + } + + + # Wrap top level arrayref in a hash + + if(UNIVERSAL::isa($ref, 'ARRAY')) { + $ref = { anon => $ref }; + } + + + # Extract rootname from top level hash if keeproot enabled + + if($self->{opt}->{keeproot}) { + my(@keys) = keys(%$ref); + if(@keys == 1) { + $ref = $ref->{$keys[0]}; + $self->{opt}->{rootname} = $keys[0]; + } + } + + # Ensure there are no top level attributes if we're not adding root elements + + elsif($self->{opt}->{rootname} eq '') { + if(UNIVERSAL::isa($ref, 'HASH')) { + my $refsave = $ref; + $ref = {}; + foreach (keys(%$refsave)) { + if(ref($refsave->{$_})) { + $ref->{$_} = $refsave->{$_}; + } + else { + $ref->{$_} = [ $refsave->{$_} ]; + } + } + } + } + + + # Encode the hashref and write to file if necessary + + $self->{_ancestors} = []; + my $xml = $self->value_to_xml($ref, $self->{opt}->{rootname}, ''); + delete $self->{_ancestors}; + + if($self->{opt}->{xmldecl}) { + $xml = $self->{opt}->{xmldecl} . "\n" . $xml; + } + + if($self->{opt}->{outputfile}) { + if(ref($self->{opt}->{outputfile})) { + my $fh = $self->{opt}->{outputfile}; + if(UNIVERSAL::isa($fh, 'GLOB') and !UNIVERSAL::can($fh, 'print')) { + eval { require IO::Handle; }; + croak $@ if $@; + } + return($fh->print($xml)); + } + else { + local(*OUT); + open(OUT, '>', "$self->{opt}->{outputfile}") || + croak "open($self->{opt}->{outputfile}): $!"; + binmode(OUT, ':utf8') if($] >= 5.008); + print OUT $xml || croak "print: $!"; + close(OUT); + } + } + elsif($self->{opt}->{handler}) { + require XML::SAX; + my $sp = XML::SAX::ParserFactory->parser( + Handler => $self->{opt}->{handler} + ); + return($sp->parse_string($xml)); + } + else { + return($xml); + } +} + + +############################################################################## +# Method: handle_options() +# +# Helper routine for both XMLin() and XMLout(). Both routines handle their +# first argument and assume all other args are options handled by this routine. +# Saves a hash of options in $self->{opt}. +# +# If default options were passed to the constructor, they will be retrieved +# here and merged with options supplied to the method call. +# +# First argument should be the string 'in' or the string 'out'. +# +# Remaining arguments should be name=>value pairs. Sets up default values +# for options not supplied. Unrecognised options are a fatal error. +# + +sub handle_options { + my $self = shift; + my $dirn = shift; + + + # Determine valid options based on context + + my %known_opt; + if($dirn eq 'in') { + @known_opt{@KnownOptIn} = @KnownOptIn; + } + else { + @known_opt{@KnownOptOut} = @KnownOptOut; + } + + + # Store supplied options in hashref and weed out invalid ones + + if(@_ % 2) { + croak "Options must be name=>value pairs (odd number supplied)"; + } + my %raw_opt = @_; + my $opt = {}; + $self->{opt} = $opt; + + while(my($key, $val) = each %raw_opt) { + my $lkey = lc($key); + $lkey =~ s/_//g; + croak "Unrecognised option: $key" unless($known_opt{$lkey}); + $opt->{$lkey} = $val; + } + + + # Merge in options passed to constructor + + foreach (keys(%known_opt)) { + unless(exists($opt->{$_})) { + if(exists($self->{def_opt}->{$_})) { + $opt->{$_} = $self->{def_opt}->{$_}; + } + } + } + + + # Set sensible defaults if not supplied + + if(exists($opt->{rootname})) { + unless(defined($opt->{rootname})) { + $opt->{rootname} = ''; + } + } + else { + $opt->{rootname} = $DefRootName; + } + + if($opt->{xmldecl} and $opt->{xmldecl} eq '1') { + $opt->{xmldecl} = $DefXmlDecl; + } + + if(exists($opt->{contentkey})) { + if($opt->{contentkey} =~ m{^-(.*)$}) { + $opt->{contentkey} = $1; + $opt->{collapseagain} = 1; + } + } + else { + $opt->{contentkey} = $DefContentKey; + } + + unless(exists($opt->{normalisespace})) { + $opt->{normalisespace} = $opt->{normalizespace}; + } + $opt->{normalisespace} = 0 unless(defined($opt->{normalisespace})); + + # Cleanups for values assumed to be arrays later + + if($opt->{searchpath}) { + unless(ref($opt->{searchpath})) { + $opt->{searchpath} = [ $opt->{searchpath} ]; + } + } + else { + $opt->{searchpath} = [ ]; + } + + if($opt->{cache} and !ref($opt->{cache})) { + $opt->{cache} = [ $opt->{cache} ]; + } + if($opt->{cache}) { + $_ = lc($_) foreach (@{$opt->{cache}}); + foreach my $scheme (@{$opt->{cache}}) { + my $method = 'cache_read_' . $scheme; + croak "Unsupported caching scheme: $scheme" + unless($self->can($method)); + } + } + + if(exists($opt->{parseropts})) { + if($^W) { + carp "Warning: " . + "'ParserOpts' is deprecated, contact the author if you need it"; + } + } + else { + $opt->{parseropts} = [ ]; + } + + + # Special cleanup for {forcearray} which could be regex, arrayref or boolean + # or left to default to 0 + + if(exists($opt->{forcearray})) { + if(ref($opt->{forcearray}) eq 'Regexp') { + $opt->{forcearray} = [ $opt->{forcearray} ]; + } + + if(ref($opt->{forcearray}) eq 'ARRAY') { + my @force_list = @{$opt->{forcearray}}; + if(@force_list) { + $opt->{forcearray} = {}; + foreach my $tag (@force_list) { + if(ref($tag) eq 'Regexp') { + push @{$opt->{forcearray}->{_regex}}, $tag; + } + else { + $opt->{forcearray}->{$tag} = 1; + } + } + } + else { + $opt->{forcearray} = 0; + } + } + else { + $opt->{forcearray} = ( $opt->{forcearray} ? 1 : 0 ); + } + } + else { + if($StrictMode and $dirn eq 'in') { + croak "No value specified for 'ForceArray' option in call to XML$dirn()"; + } + $opt->{forcearray} = 0; + } + + + # Special cleanup for {keyattr} which could be arrayref or hashref or left + # to default to arrayref + + if(exists($opt->{keyattr})) { + if(ref($opt->{keyattr})) { + if(ref($opt->{keyattr}) eq 'HASH') { + + # Make a copy so we can mess with it + + $opt->{keyattr} = { %{$opt->{keyattr}} }; + + + # Convert keyattr => { elem => '+attr' } + # to keyattr => { elem => [ 'attr', '+' ] } + + foreach my $el (keys(%{$opt->{keyattr}})) { + if($opt->{keyattr}->{$el} =~ /^(\+|-)?(.*)$/) { + $opt->{keyattr}->{$el} = [ $2, ($1 ? $1 : '') ]; + if($StrictMode and $dirn eq 'in') { + next if($opt->{forcearray} == 1); + next if(ref($opt->{forcearray}) eq 'HASH' + and $opt->{forcearray}->{$el}); + croak "<$el> set in KeyAttr but not in ForceArray"; + } + } + else { + delete($opt->{keyattr}->{$el}); # Never reached (famous last words?) + } + } + } + else { + if(@{$opt->{keyattr}} == 0) { + delete($opt->{keyattr}); + } + } + } + else { + $opt->{keyattr} = [ $opt->{keyattr} ]; + } + } + else { + if($StrictMode) { + croak "No value specified for 'KeyAttr' option in call to XML$dirn()"; + } + $opt->{keyattr} = [ @DefKeyAttr ]; + } + + + # Special cleanup for {valueattr} which could be arrayref or hashref + + if(exists($opt->{valueattr})) { + if(ref($opt->{valueattr}) eq 'ARRAY') { + $opt->{valueattrlist} = {}; + $opt->{valueattrlist}->{$_} = 1 foreach(@{ delete $opt->{valueattr} }); + } + } + + # make sure there's nothing weird in {grouptags} + + if($opt->{grouptags}) { + croak "Illegal value for 'GroupTags' option - expected a hashref" + unless UNIVERSAL::isa($opt->{grouptags}, 'HASH'); + + while(my($key, $val) = each %{$opt->{grouptags}}) { + next if $key ne $val; + croak "Bad value in GroupTags: '$key' => '$val'"; + } + } + + + # Check the {variables} option is valid and initialise variables hash + + if($opt->{variables} and !UNIVERSAL::isa($opt->{variables}, 'HASH')) { + croak "Illegal value for 'Variables' option - expected a hashref"; + } + + if($opt->{variables}) { + $self->{_var_values} = { %{$opt->{variables}} }; + } + elsif($opt->{varattr}) { + $self->{_var_values} = {}; + } + +} + + +############################################################################## +# Method: find_xml_file() +# +# Helper routine for XMLin(). +# Takes a filename, and a list of directories, attempts to locate the file in +# the directories listed. +# Returns a full pathname on success; croaks on failure. +# + +sub find_xml_file { + my $self = shift; + my $file = shift; + my @search_path = @_; + + + require File::Basename; + require File::Spec; + + my($filename, $filedir) = File::Basename::fileparse($file); + + if($filename ne $file) { # Ignore searchpath if dir component + return($file) if(-e $file); + } + else { + my($path); + foreach $path (@search_path) { + my $fullpath = File::Spec->catfile($path, $file); + return($fullpath) if(-e $fullpath); + } + } + + # If user did not supply a search path, default to current directory + + if(!@search_path) { + return($file) if(-e $file); + croak "File does not exist: $file"; + } + + croak "Could not find $file in ", join(':', @search_path); +} + + +############################################################################## +# Method: collapse() +# +# Helper routine for XMLin(). This routine really comprises the 'smarts' (or +# value add) of this module. +# +# Takes the parse tree that XML::Parser produced from the supplied XML and +# recurses through it 'collapsing' unnecessary levels of indirection (nested +# arrays etc) to produce a data structure that is easier to work with. +# +# Elements in the original parser tree are represented as an element name +# followed by an arrayref. The first element of the array is a hashref +# containing the attributes. The rest of the array contains a list of any +# nested elements as name+arrayref pairs: +# +# , [ { }, , [ ... ], ... ] +# +# The special element name '0' (zero) flags text content. +# +# This routine cuts down the noise by discarding any text content consisting of +# only whitespace and then moves the nested elements into the attribute hash +# using the name of the nested element as the hash key and the collapsed +# version of the nested element as the value. Multiple nested elements with +# the same name will initially be represented as an arrayref, but this may be +# 'folded' into a hashref depending on the value of the keyattr option. +# + +sub collapse { + my $self = shift; + + + # Start with the hash of attributes + + my $attr = shift; + if($self->{opt}->{noattr}) { # Discard if 'noattr' set + $attr = {}; + } + elsif($self->{opt}->{normalisespace} == 2) { + while(my($key, $value) = each %$attr) { + $attr->{$key} = $self->normalise_space($value) + } + } + + + # Do variable substitutions + + if(my $var = $self->{_var_values}) { + while(my($key, $val) = each(%$attr)) { + $val =~ s{\$\{([\w.]+)\}}{ $self->get_var($1) }ge; + $attr->{$key} = $val; + } + } + + + # Roll up 'value' attributes (but only if no nested elements) + + if(!@_ and keys %$attr == 1) { + my($k) = keys %$attr; + if($self->{opt}->{valueattrlist} and $self->{opt}->{valueattrlist}->{$k}) { + return $attr->{$k}; + } + } + + + # Add any nested elements + + my($key, $val); + while(@_) { + $key = shift; + $val = shift; + + if(ref($val)) { + $val = $self->collapse(@$val); + next if(!defined($val) and $self->{opt}->{suppressempty}); + } + elsif($key eq '0') { + next if($val =~ m{^\s*$}s); # Skip all whitespace content + + $val = $self->normalise_space($val) + if($self->{opt}->{normalisespace} == 2); + + # do variable substitutions + + if(my $var = $self->{_var_values}) { + $val =~ s{\$\{(\w+)\}}{ $self->get_var($1) }ge; + } + + + # look for variable definitions + + if(my $var = $self->{opt}->{varattr}) { + if(exists $attr->{$var}) { + $self->set_var($attr->{$var}, $val); + } + } + + + # Collapse text content in element with no attributes to a string + + if(!%$attr and !@_) { + return($self->{opt}->{forcecontent} ? + { $self->{opt}->{contentkey} => $val } : $val + ); + } + $key = $self->{opt}->{contentkey}; + } + + + # Combine duplicate attributes into arrayref if required + + if(exists($attr->{$key})) { + if(UNIVERSAL::isa($attr->{$key}, 'ARRAY')) { + push(@{$attr->{$key}}, $val); + } + else { + $attr->{$key} = [ $attr->{$key}, $val ]; + } + } + elsif(defined($val) and UNIVERSAL::isa($val, 'ARRAY')) { + $attr->{$key} = [ $val ]; + } + else { + if( $key ne $self->{opt}->{contentkey} + and ( + ($self->{opt}->{forcearray} == 1) + or ( + (ref($self->{opt}->{forcearray}) eq 'HASH') + and ( + $self->{opt}->{forcearray}->{$key} + or (grep $key =~ $_, @{$self->{opt}->{forcearray}->{_regex}}) + ) + ) + ) + ) { + $attr->{$key} = [ $val ]; + } + else { + $attr->{$key} = $val; + } + } + + } + + + # Turn arrayrefs into hashrefs if key fields present + + if($self->{opt}->{keyattr}) { + while(($key,$val) = each %$attr) { + if(defined($val) and UNIVERSAL::isa($val, 'ARRAY')) { + $attr->{$key} = $self->array_to_hash($key, $val); + } + } + } + + + # disintermediate grouped tags + + if($self->{opt}->{grouptags}) { + while(my($key, $val) = each(%$attr)) { + next unless(UNIVERSAL::isa($val, 'HASH') and (keys %$val == 1)); + next unless(exists($self->{opt}->{grouptags}->{$key})); + + my($child_key, $child_val) = %$val; + + if($self->{opt}->{grouptags}->{$key} eq $child_key) { + $attr->{$key}= $child_val; + } + } + } + + + # Fold hashes containing a single anonymous array up into just the array + + my $count = scalar keys %$attr; + if($count == 1 + and exists $attr->{anon} + and UNIVERSAL::isa($attr->{anon}, 'ARRAY') + ) { + return($attr->{anon}); + } + + + # Do the right thing if hash is empty, otherwise just return it + + if(!%$attr and exists($self->{opt}->{suppressempty})) { + if(defined($self->{opt}->{suppressempty}) and + $self->{opt}->{suppressempty} eq '') { + return(''); + } + return(undef); + } + + + # Roll up named elements with named nested 'value' attributes + + if($self->{opt}->{valueattr}) { + while(my($key, $val) = each(%$attr)) { + next unless($self->{opt}->{valueattr}->{$key}); + next unless(UNIVERSAL::isa($val, 'HASH') and (keys %$val == 1)); + my($k) = keys %$val; + next unless($k eq $self->{opt}->{valueattr}->{$key}); + $attr->{$key} = $val->{$k}; + } + } + + return($attr) + +} + + +############################################################################## +# Method: set_var() +# +# Called when a variable definition is encountered in the XML. (A variable +# definition looks like value where attrname +# matches the varattr setting). +# + +sub set_var { + my($self, $name, $value) = @_; + + $self->{_var_values}->{$name} = $value; +} + + +############################################################################## +# Method: get_var() +# +# Called during variable substitution to get the value for the named variable. +# + +sub get_var { + my($self, $name) = @_; + + my $value = $self->{_var_values}->{$name}; + return $value if(defined($value)); + + return '${' . $name . '}'; +} + + +############################################################################## +# Method: normalise_space() +# +# Strips leading and trailing whitespace and collapses sequences of whitespace +# characters to a single space. +# + +sub normalise_space { + my($self, $text) = @_; + + $text =~ s/^\s+//s; + $text =~ s/\s+$//s; + $text =~ s/\s\s+/ /sg; + + return $text; +} + + +############################################################################## +# Method: array_to_hash() +# +# Helper routine for collapse(). +# Attempts to 'fold' an array of hashes into an hash of hashes. Returns a +# reference to the hash on success or the original array if folding is +# not possible. Behaviour is controlled by 'keyattr' option. +# + +sub array_to_hash { + my $self = shift; + my $name = shift; + my $arrayref = shift; + + my $hashref = $self->new_hashref; + + my($i, $key, $val, $flag); + + + # Handle keyattr => { .... } + + if(ref($self->{opt}->{keyattr}) eq 'HASH') { + return($arrayref) unless(exists($self->{opt}->{keyattr}->{$name})); + ($key, $flag) = @{$self->{opt}->{keyattr}->{$name}}; + for($i = 0; $i < @$arrayref; $i++) { + if(UNIVERSAL::isa($arrayref->[$i], 'HASH') and + exists($arrayref->[$i]->{$key}) + ) { + $val = $arrayref->[$i]->{$key}; + if(ref($val)) { + $self->die_or_warn("<$name> element has non-scalar '$key' key attribute"); + return($arrayref); + } + $val = $self->normalise_space($val) + if($self->{opt}->{normalisespace} == 1); + $self->die_or_warn("<$name> element has non-unique value in '$key' key attribute: $val") + if(exists($hashref->{$val})); + $hashref->{$val} = { %{$arrayref->[$i]} }; + $hashref->{$val}->{"-$key"} = $hashref->{$val}->{$key} if($flag eq '-'); + delete $hashref->{$val}->{$key} unless($flag eq '+'); + } + else { + $self->die_or_warn("<$name> element has no '$key' key attribute"); + return($arrayref); + } + } + } + + + # Or assume keyattr => [ .... ] + + else { + my $default_keys = + join(',', @DefKeyAttr) eq join(',', @{$self->{opt}->{keyattr}}); + + ELEMENT: for($i = 0; $i < @$arrayref; $i++) { + return($arrayref) unless(UNIVERSAL::isa($arrayref->[$i], 'HASH')); + + foreach $key (@{$self->{opt}->{keyattr}}) { + if(defined($arrayref->[$i]->{$key})) { + $val = $arrayref->[$i]->{$key}; + if(ref($val)) { + $self->die_or_warn("<$name> element has non-scalar '$key' key attribute") + if not $default_keys; + return($arrayref); + } + $val = $self->normalise_space($val) + if($self->{opt}->{normalisespace} == 1); + $self->die_or_warn("<$name> element has non-unique value in '$key' key attribute: $val") + if(exists($hashref->{$val})); + $hashref->{$val} = { %{$arrayref->[$i]} }; + delete $hashref->{$val}->{$key}; + next ELEMENT; + } + } + + return($arrayref); # No keyfield matched + } + } + + # collapse any hashes which now only have a 'content' key + + if($self->{opt}->{collapseagain}) { + $hashref = $self->collapse_content($hashref); + } + + return($hashref); +} + + +############################################################################## +# Method: die_or_warn() +# +# Takes a diagnostic message and does one of three things: +# 1. dies if strict mode is enabled +# 2. warns if warnings are enabled but strict mode is not +# 3. ignores message and resturns silently if neither strict mode nor warnings +# are enabled +# + +sub die_or_warn { + my $self = shift; + my $msg = shift; + + croak $msg if($StrictMode); + carp "Warning: $msg" if($^W); +} + + +############################################################################## +# Method: new_hashref() +# +# This is a hook routine for overriding in a sub-class. Some people believe +# that using Tie::IxHash here will solve order-loss problems. +# + +sub new_hashref { + my $self = shift; + + return { @_ }; +} + + +############################################################################## +# Method: collapse_content() +# +# Helper routine for array_to_hash +# +# Arguments expected are: +# - an XML::Simple object +# - a hasref +# the hashref is a former array, turned into a hash by array_to_hash because +# of the presence of key attributes +# at this point collapse_content avoids over-complicated structures like +# dir => { libexecdir => { content => '$exec_prefix/libexec' }, +# localstatedir => { content => '$prefix' }, +# } +# into +# dir => { libexecdir => '$exec_prefix/libexec', +# localstatedir => '$prefix', +# } + +sub collapse_content { + my $self = shift; + my $hashref = shift; + + my $contentkey = $self->{opt}->{contentkey}; + + # first go through the values,checking that they are fit to collapse + foreach my $val (values %$hashref) { + return $hashref unless ( (ref($val) eq 'HASH') + and (keys %$val == 1) + and (exists $val->{$contentkey}) + ); + } + + # now collapse them + foreach my $key (keys %$hashref) { + $hashref->{$key}= $hashref->{$key}->{$contentkey}; + } + + return $hashref; +} + + +############################################################################## +# Method: value_to_xml() +# +# Helper routine for XMLout() - recurses through a data structure building up +# and returning an XML representation of that structure as a string. +# +# Arguments expected are: +# - the data structure to be encoded (usually a reference) +# - the XML tag name to use for this item +# - a string of spaces for use as the current indent level +# + +sub value_to_xml { + my $self = shift;; + + + # Grab the other arguments + + my($ref, $name, $indent) = @_; + + my $named = (defined($name) and $name ne '' ? 1 : 0); + + my $nl = "\n"; + + my $is_root = $indent eq '' ? 1 : 0; # Warning, dirty hack! + if($self->{opt}->{noindent}) { + $indent = ''; + $nl = ''; + } + + + # Convert to XML + + if(ref($ref)) { + croak "circular data structures not supported" + if(grep($_ == $ref, @{$self->{_ancestors}})); + push @{$self->{_ancestors}}, $ref; + } + else { + if($named) { + return(join('', + $indent, '<', $name, '>', + ($self->{opt}->{noescape} ? $ref : $self->escape_value($ref)), + '", $nl + )); + } + else { + return("$ref$nl"); + } + } + + + # Unfold hash to array if possible + + if(UNIVERSAL::isa($ref, 'HASH') # It is a hash + and keys %$ref # and it's not empty + and $self->{opt}->{keyattr} # and folding is enabled + and !$is_root # and its not the root element + ) { + $ref = $self->hash_to_array($name, $ref); + } + + + my @result = (); + my($key, $value); + + + # Handle hashrefs + + if(UNIVERSAL::isa($ref, 'HASH')) { + + # Reintermediate grouped values if applicable + + if($self->{opt}->{grouptags}) { + $ref = $self->copy_hash($ref); + while(my($key, $val) = each %$ref) { + if($self->{opt}->{grouptags}->{$key}) { + $ref->{$key} = { $self->{opt}->{grouptags}->{$key} => $val }; + } + } + } + + + # Scan for namespace declaration attributes + + my $nsdecls = ''; + my $default_ns_uri; + if($self->{nsup}) { + $ref = $self->copy_hash($ref); + $self->{nsup}->push_context(); + + # Look for default namespace declaration first + + if(exists($ref->{xmlns})) { + $self->{nsup}->declare_prefix('', $ref->{xmlns}); + $nsdecls .= qq( xmlns="$ref->{xmlns}"); + delete($ref->{xmlns}); + } + $default_ns_uri = $self->{nsup}->get_uri(''); + + + # Then check all the other keys + + foreach my $qname (keys(%$ref)) { + my($uri, $lname) = $self->{nsup}->parse_jclark_notation($qname); + if($uri) { + if($uri eq $xmlns_ns) { + $self->{nsup}->declare_prefix($lname, $ref->{$qname}); + $nsdecls .= qq( xmlns:$lname="$ref->{$qname}"); + delete($ref->{$qname}); + } + } + } + + # Translate any remaining Clarkian names + + foreach my $qname (keys(%$ref)) { + my($uri, $lname) = $self->{nsup}->parse_jclark_notation($qname); + if($uri) { + if($default_ns_uri and $uri eq $default_ns_uri) { + $ref->{$lname} = $ref->{$qname}; + delete($ref->{$qname}); + } + else { + my $prefix = $self->{nsup}->get_prefix($uri); + unless($prefix) { + # $self->{nsup}->declare_prefix(undef, $uri); + # $prefix = $self->{nsup}->get_prefix($uri); + $prefix = $self->{ns_prefix}++; + $self->{nsup}->declare_prefix($prefix, $uri); + $nsdecls .= qq( xmlns:$prefix="$uri"); + } + $ref->{"$prefix:$lname"} = $ref->{$qname}; + delete($ref->{$qname}); + } + } + } + } + + + my @nested = (); + my $text_content = undef; + if($named) { + push @result, $indent, '<', $name, $nsdecls; + } + + if(keys %$ref) { + my $first_arg = 1; + foreach my $key ($self->sorted_keys($name, $ref)) { + my $value = $ref->{$key}; + next if(substr($key, 0, 1) eq '-'); + if(!defined($value)) { + next if $self->{opt}->{suppressempty}; + unless(exists($self->{opt}->{suppressempty}) + and !defined($self->{opt}->{suppressempty}) + ) { + carp 'Use of uninitialized value' if($^W); + } + if($key eq $self->{opt}->{contentkey}) { + $text_content = ''; + } + else { + $value = exists($self->{opt}->{suppressempty}) ? {} : ''; + } + } + + if(!ref($value) + and $self->{opt}->{valueattr} + and $self->{opt}->{valueattr}->{$key} + ) { + $value = { $self->{opt}->{valueattr}->{$key} => $value }; + } + + if(ref($value) or $self->{opt}->{noattr}) { + push @nested, + $self->value_to_xml($value, $key, "$indent "); + } + else { + $value = $self->escape_value($value) unless($self->{opt}->{noescape}); + if($key eq $self->{opt}->{contentkey}) { + $text_content = $value; + } + else { + push @result, "\n$indent " . ' ' x length($name) + if($self->{opt}->{attrindent} and !$first_arg); + push @result, ' ', $key, '="', $value , '"'; + $first_arg = 0; + } + } + } + } + else { + $text_content = ''; + } + + if(@nested or defined($text_content)) { + if($named) { + push @result, ">"; + if(defined($text_content)) { + push @result, $text_content; + $nested[0] =~ s/^\s+// if(@nested); + } + else { + push @result, $nl; + } + if(@nested) { + push @result, @nested, $indent; + } + push @result, '", $nl; + } + else { + push @result, @nested; # Special case if no root elements + } + } + else { + push @result, " />", $nl; + } + $self->{nsup}->pop_context() if($self->{nsup}); + } + + + # Handle arrayrefs + + elsif(UNIVERSAL::isa($ref, 'ARRAY')) { + foreach $value (@$ref) { + next if !defined($value) and $self->{opt}->{suppressempty}; + if(!ref($value)) { + push @result, + $indent, '<', $name, '>', + ($self->{opt}->{noescape} ? $value : $self->escape_value($value)), + '$nl"; + } + elsif(UNIVERSAL::isa($value, 'HASH')) { + push @result, $self->value_to_xml($value, $name, $indent); + } + else { + push @result, + $indent, '<', $name, ">$nl", + $self->value_to_xml($value, 'anon', "$indent "), + $indent, '$nl"; + } + } + } + + else { + croak "Can't encode a value of type: " . ref($ref); + } + + + pop @{$self->{_ancestors}} if(ref($ref)); + + return(join('', @result)); +} + + +############################################################################## +# Method: sorted_keys() +# +# Returns the keys of the referenced hash sorted into alphabetical order, but +# with the 'key' key (as in KeyAttr) first, if there is one. +# + +sub sorted_keys { + my($self, $name, $ref) = @_; + + return keys %$ref if $self->{opt}->{nosort}; + + my %hash = %$ref; + my $keyattr = $self->{opt}->{keyattr}; + + my @key; + + if(ref $keyattr eq 'HASH') { + if(exists $keyattr->{$name} and exists $hash{$keyattr->{$name}->[0]}) { + push @key, $keyattr->{$name}->[0]; + delete $hash{$keyattr->{$name}->[0]}; + } + } + elsif(ref $keyattr eq 'ARRAY') { + foreach (@{$keyattr}) { + if(exists $hash{$_}) { + push @key, $_; + delete $hash{$_}; + last; + } + } + } + + return(@key, sort keys %hash); +} + +############################################################################## +# Method: escape_value() +# +# Helper routine for automatically escaping values for XMLout(). +# Expects a scalar data value. Returns escaped version. +# + +sub escape_value { + my($self, $data) = @_; + + return '' unless(defined($data)); + + $data =~ s/&/&/sg; + $data =~ s//>/sg; + $data =~ s/"/"/sg; + + my $level = $self->{opt}->{numericescape} or return $data; + + return $self->numeric_escape($data, $level); +} + +sub numeric_escape { + my($self, $data, $level) = @_; + + use utf8; # required for 5.6 + + if($self->{opt}->{numericescape} eq '2') { + $data =~ s/([^\x00-\x7F])/'&#' . ord($1) . ';'/gse; + } + else { + $data =~ s/([^\x00-\xFF])/'&#' . ord($1) . ';'/gse; + } + + return $data; +} + + +############################################################################## +# Method: hash_to_array() +# +# Helper routine for value_to_xml(). +# Attempts to 'unfold' a hash of hashes into an array of hashes. Returns a +# reference to the array on success or the original hash if unfolding is +# not possible. +# + +sub hash_to_array { + my $self = shift; + my $parent = shift; + my $hashref = shift; + + my $arrayref = []; + + my($key, $value); + + my @keys = $self->{opt}->{nosort} ? keys %$hashref : sort keys %$hashref; + foreach $key (@keys) { + $value = $hashref->{$key}; + return($hashref) unless(UNIVERSAL::isa($value, 'HASH')); + + if(ref($self->{opt}->{keyattr}) eq 'HASH') { + return($hashref) unless(defined($self->{opt}->{keyattr}->{$parent})); + push @$arrayref, $self->copy_hash( + $value, $self->{opt}->{keyattr}->{$parent}->[0] => $key + ); + } + else { + push(@$arrayref, { $self->{opt}->{keyattr}->[0] => $key, %$value }); + } + } + + return($arrayref); +} + + +############################################################################## +# Method: copy_hash() +# +# Helper routine for hash_to_array(). When unfolding a hash of hashes into +# an array of hashes, we need to copy the key from the outer hash into the +# inner hash. This routine makes a copy of the original hash so we don't +# destroy the original data structure. You might wish to override this +# method if you're using tied hashes and don't want them to get untied. +# + +sub copy_hash { + my($self, $orig, @extra) = @_; + + return { @extra, %$orig }; +} + +############################################################################## +# Methods required for building trees from SAX events +############################################################################## + +sub start_document { + my $self = shift; + + $self->handle_options('in') unless($self->{opt}); + + $self->{lists} = []; + $self->{curlist} = $self->{tree} = []; +} + + +sub start_element { + my $self = shift; + my $element = shift; + + my $name = $element->{Name}; + if($self->{opt}->{nsexpand}) { + $name = $element->{LocalName} || ''; + if($element->{NamespaceURI}) { + $name = '{' . $element->{NamespaceURI} . '}' . $name; + } + } + my $attributes = {}; + if($element->{Attributes}) { # Might be undef + foreach my $attr (values %{$element->{Attributes}}) { + if($self->{opt}->{nsexpand}) { + my $name = $attr->{LocalName} || ''; + if($attr->{NamespaceURI}) { + $name = '{' . $attr->{NamespaceURI} . '}' . $name + } + $name = 'xmlns' if($name eq $bad_def_ns_jcn); + $attributes->{$name} = $attr->{Value}; + } + else { + $attributes->{$attr->{Name}} = $attr->{Value}; + } + } + } + my $newlist = [ $attributes ]; + push @{ $self->{lists} }, $self->{curlist}; + push @{ $self->{curlist} }, $name => $newlist; + $self->{curlist} = $newlist; +} + + +sub characters { + my $self = shift; + my $chars = shift; + + my $text = $chars->{Data}; + my $clist = $self->{curlist}; + my $pos = $#$clist; + + if ($pos > 0 and $clist->[$pos - 1] eq '0') { + $clist->[$pos] .= $text; + } + else { + push @$clist, 0 => $text; + } +} + + +sub end_element { + my $self = shift; + + $self->{curlist} = pop @{ $self->{lists} }; +} + + +sub end_document { + my $self = shift; + + delete($self->{curlist}); + delete($self->{lists}); + + my $tree = $self->{tree}; + delete($self->{tree}); + + + # Return tree as-is to XMLin() + + return($tree) if($self->{nocollapse}); + + + # Or collapse it before returning it to SAX parser class + + if($self->{opt}->{keeproot}) { + $tree = $self->collapse({}, @$tree); + } + else { + $tree = $self->collapse(@{$tree->[1]}); + } + + if($self->{opt}->{datahandler}) { + return($self->{opt}->{datahandler}->($self, $tree)); + } + + return($tree); +} + +*xml_in = \&XMLin; +*xml_out = \&XMLout; + +1; + +__END__ + +=head1 QUICK START + +Say you have a script called B and a file of configuration options +called B containing this: + + + +
10.0.0.101
+
10.0.1.101
+
+ +
10.0.0.102
+
+ +
10.0.0.103
+
10.0.1.103
+
+
+ +The following lines of code in B: + + use XML::Simple; + + my $config = XMLin(); + +will 'slurp' the configuration options into the hashref $config (because no +arguments are passed to C the name and location of the XML file will +be inferred from name and location of the script). You can dump out the +contents of the hashref using Data::Dumper: + + use Data::Dumper; + + print Dumper($config); + +which will produce something like this (formatting has been adjusted for +brevity): + + { + 'logdir' => '/var/log/foo/', + 'debugfile' => '/tmp/foo.debug', + 'server' => { + 'sahara' => { + 'osversion' => '2.6', + 'osname' => 'solaris', + 'address' => [ '10.0.0.101', '10.0.1.101' ] + }, + 'gobi' => { + 'osversion' => '6.5', + 'osname' => 'irix', + 'address' => '10.0.0.102' + }, + 'kalahari' => { + 'osversion' => '2.0.34', + 'osname' => 'linux', + 'address' => [ '10.0.0.103', '10.0.1.103' ] + } + } + } + +Your script could then access the name of the log directory like this: + + print $config->{logdir}; + +similarly, the second address on the server 'kalahari' could be referenced as: + + print $config->{server}->{kalahari}->{address}->[1]; + +What could be simpler? (Rhetorical). + +For simple requirements, that's really all there is to it. If you want to +store your XML in a different directory or file, or pass it in as a string or +even pass it in via some derivative of an IO::Handle, you'll need to check out +L<"OPTIONS">. If you want to turn off or tweak the array folding feature (that +neat little transformation that produced $config->{server}) you'll find options +for that as well. + +If you want to generate XML (for example to write a modified version of +$config back out as XML), check out C. + +If your needs are not so simple, this may not be the module for you. In that +case, you might want to read L<"WHERE TO FROM HERE?">. + +=head1 DESCRIPTION + +The XML::Simple module provides a simple API layer on top of an underlying XML +parsing module (either XML::Parser or one of the SAX2 parser modules). Two +functions are exported: C and C. Note: you can explicity +request the lower case versions of the function names: C and +C. + +The simplest approach is to call these two functions directly, but an +optional object oriented interface (see L<"OPTIONAL OO INTERFACE"> below) +allows them to be called as methods of an B object. The object +interface can also be used at either end of a SAX pipeline. + +=head2 XMLin() + +Parses XML formatted data and returns a reference to a data structure which +contains the same information in a more readily accessible form. (Skip +down to L<"EXAMPLES"> below, for more sample code). + +C accepts an optional XML specifier followed by zero or more 'name => +value' option pairs. The XML specifier can be one of the following: + +=over 4 + +=item A filename + +If the filename contains no directory components C will look for the +file in each directory in the SearchPath (see L<"OPTIONS"> below) or in the +current directory if the SearchPath option is not defined. eg: + + $ref = XMLin('/etc/params.xml'); + +Note, the filename '-' can be used to parse from STDIN. + +=item undef + +If there is no XML specifier, C will check the script directory and +each of the SearchPath directories for a file with the same name as the script +but with the extension '.xml'. Note: if you wish to specify options, you +must specify the value 'undef'. eg: + + $ref = XMLin(undef, ForceArray => 1); + +=item A string of XML + +A string containing XML (recognised by the presence of '<' and '>' characters) +will be parsed directly. eg: + + $ref = XMLin(''); + +=item An IO::Handle object + +An IO::Handle object will be read to EOF and its contents parsed. eg: + + $fh = IO::File->new('/etc/params.xml'); + $ref = XMLin($fh); + +=back + +=head2 XMLout() + +Takes a data structure (generally a hashref) and returns an XML encoding of +that structure. If the resulting XML is parsed using C, it should +return a data structure equivalent to the original (see caveats below). + +The C function can also be used to output the XML as SAX events +see the C option and L<"SAX SUPPORT"> for more details). + +When translating hashes to XML, hash keys which have a leading '-' will be +silently skipped. This is the approved method for marking elements of a +data structure which should be ignored by C. (Note: If these items +were not skipped the key names would be emitted as element or attribute names +with a leading '-' which would not be valid XML). + +=head2 Caveats + +Some care is required in creating data structures which will be passed to +C. Hash keys from the data structure will be encoded as either XML +element names or attribute names. Therefore, you should use hash key names +which conform to the relatively strict XML naming rules: + +Names in XML must begin with a letter. The remaining characters may be +letters, digits, hyphens (-), underscores (_) or full stops (.). It is also +allowable to include one colon (:) in an element name but this should only be +used when working with namespaces (B can only usefully work with +namespaces when teamed with a SAX Parser). + +You can use other punctuation characters in hash values (just not in hash +keys) however B does not support dumping binary data. + +If you break these rules, the current implementation of C will +simply emit non-compliant XML which will be rejected if you try to read it +back in. (A later version of B might take a more proactive +approach). + +Note also that although you can nest hashes and arrays to arbitrary levels, +circular data structures are not supported and will cause C to die. + +If you wish to 'round-trip' arbitrary data structures from Perl to XML and back +to Perl, then you should probably disable array folding (using the KeyAttr +option) both with C and with C. If you still don't get the +expected results, you may prefer to use L which is designed for +exactly that purpose. + +Refer to L<"WHERE TO FROM HERE?"> if C is too simple for your needs. + + +=head1 OPTIONS + +B supports a number of options (in fact as each release of +B adds more options, the module's claim to the name 'Simple' +becomes increasingly tenuous). If you find yourself repeatedly having to +specify the same options, you might like to investigate L<"OPTIONAL OO +INTERFACE"> below. + +If you can't be bothered reading the documentation, refer to +L<"STRICT MODE"> to automatically catch common mistakes. + +Because there are so many options, it's hard for new users to know which ones +are important, so here are the two you really need to know about: + +=over 4 + +=item * + +check out C because you'll almost certainly want to turn it on + +=item * + +make sure you know what the C option does and what its default value is +because it may surprise you otherwise (note in particular that 'KeyAttr' +affects both C and C) + +=back + +The option name headings below have a trailing 'comment' - a hash followed by +two pieces of metadata: + +=over 4 + +=item * + +Options are marked with 'I' if they are recognised by C and +'I' if they are recognised by C. + +=item * + +Each option is also flagged to indicate whether it is: + + 'important' - don't use the module until you understand this one + 'handy' - you can skip this on the first time through + 'advanced' - you can skip this on the second time through + 'SAX only' - don't worry about this unless you're using SAX (or + alternatively if you need this, you also need SAX) + 'seldom used' - you'll probably never use this unless you were the + person that requested the feature + +=back + +The options are listed alphabetically: + +Note: option names are no longer case sensitive so you can use the mixed case +versions shown here; all lower case as required by versions 2.03 and earlier; +or you can add underscores between the words (eg: key_attr). + + +=head2 AttrIndent => 1 I<# out - handy> + +When you are using C, enable this option to have attributes printed +one-per-line with sensible indentation rather than all on one line. + +=head2 Cache => [ cache schemes ] I<# in - advanced> + +Because loading the B module and parsing an XML file can consume a +significant number of CPU cycles, it is often desirable to cache the output of +C for later reuse. + +When parsing from a named file, B supports a number of caching +schemes. The 'Cache' option may be used to specify one or more schemes (using +an anonymous array). Each scheme will be tried in turn in the hope of finding +a cached pre-parsed representation of the XML file. If no cached copy is +found, the file will be parsed and the first cache scheme in the list will be +used to save a copy of the results. The following cache schemes have been +implemented: + +=over 4 + +=item storable + +Utilises B to read/write a cache file with the same name as the +XML file but with the extension .stor + +=item memshare + +When a file is first parsed, a copy of the resulting data structure is retained +in memory in the B module's namespace. Subsequent calls to parse +the same file will return a reference to this structure. This cached version +will persist only for the life of the Perl interpreter (which in the case of +mod_perl for example, may be some significant time). + +Because each caller receives a reference to the same data structure, a change +made by one caller will be visible to all. For this reason, the reference +returned should be treated as read-only. + +=item memcopy + +This scheme works identically to 'memshare' (above) except that each caller +receives a reference to a new data structure which is a copy of the cached +version. Copying the data structure will add a little processing overhead, +therefore this scheme should only be used where the caller intends to modify +the data structure (or wishes to protect itself from others who might). This +scheme uses B to perform the copy. + +=back + +Warning! The memory-based caching schemes compare the timestamp on the file to +the time when it was last parsed. If the file is stored on an NFS filesystem +(or other network share) and the clock on the file server is not exactly +synchronised with the clock where your script is run, updates to the source XML +file may appear to be ignored. + +=head2 ContentKey => 'keyname' I<# in+out - seldom used> + +When text content is parsed to a hash value, this option let's you specify a +name for the hash key to override the default 'content'. So for example: + + XMLin('Text', ContentKey => 'text') + +will parse to: + + { 'one' => 1, 'text' => 'Text' } + +instead of: + + { 'one' => 1, 'content' => 'Text' } + +C will also honour the value of this option when converting a hashref +to XML. + +You can also prefix your selected key name with a '-' character to have +C try a little harder to eliminate unnecessary 'content' keys after +array folding. For example: + + XMLin( + 'FirstSecond', + KeyAttr => {item => 'name'}, + ForceArray => [ 'item' ], + ContentKey => '-content' + ) + +will parse to: + + { + 'item' => { + 'one' => 'First' + 'two' => 'Second' + } + } + +rather than this (without the '-'): + + { + 'item' => { + 'one' => { 'content' => 'First' } + 'two' => { 'content' => 'Second' } + } + } + +=head2 DataHandler => code_ref I<# in - SAX only> + +When you use an B object as a SAX handler, it will return a +'simple tree' data structure in the same format as C would return. If +this option is set (to a subroutine reference), then when the tree is built the +subroutine will be called and passed two arguments: a reference to the +B object and a reference to the data tree. The return value from +the subroutine will be returned to the SAX driver. (See L<"SAX SUPPORT"> for +more details). + +=head2 ForceArray => 1 I<# in - important> + +This option should be set to '1' to force nested elements to be represented +as arrays even when there is only one. Eg, with ForceArray enabled, this +XML: + + + value + + +would parse to this: + + { + 'name' => [ + 'value' + ] + } + +instead of this (the default): + + { + 'name' => 'value' + } + +This option is especially useful if the data structure is likely to be written +back out as XML and the default behaviour of rolling single nested elements up +into attributes is not desirable. + +If you are using the array folding feature, you should almost certainly enable +this option. If you do not, single nested elements will not be parsed to +arrays and therefore will not be candidates for folding to a hash. (Given that +the default value of 'KeyAttr' enables array folding, the default value of this +option should probably also have been enabled too - sorry). + +=head2 ForceArray => [ names ] I<# in - important> + +This alternative (and preferred) form of the 'ForceArray' option allows you to +specify a list of element names which should always be forced into an array +representation, rather than the 'all or nothing' approach above. + +It is also possible (since version 2.05) to include compiled regular +expressions in the list - any element names which match the pattern will be +forced to arrays. If the list contains only a single regex, then it is not +necessary to enclose it in an arrayref. Eg: + + ForceArray => qr/_list$/ + +=head2 ForceContent => 1 I<# in - seldom used> + +When C parses elements which have text content as well as attributes, +the text content must be represented as a hash value rather than a simple +scalar. This option allows you to force text content to always parse to +a hash value even when there are no attributes. So for example: + + XMLin('text1text2', ForceContent => 1) + +will parse to: + + { + 'x' => { 'content' => 'text1' }, + 'y' => { 'a' => 2, 'content' => 'text2' } + } + +instead of: + + { + 'x' => 'text1', + 'y' => { 'a' => 2, 'content' => 'text2' } + } + +=head2 GroupTags => { grouping tag => grouped tag } I<# in+out - handy> + +You can use this option to eliminate extra levels of indirection in your Perl +data structure. For example this XML: + + + + /usr/bin + /usr/local/bin + /usr/X11/bin + + + +Would normally be read into a structure like this: + + { + searchpath => { + dir => [ '/usr/bin', '/usr/local/bin', '/usr/X11/bin' ] + } + } + +But when read in with the appropriate value for 'GroupTags': + + my $opt = XMLin($xml, GroupTags => { searchpath => 'dir' }); + +It will return this simpler structure: + + { + searchpath => [ '/usr/bin', '/usr/local/bin', '/usr/X11/bin' ] + } + +The grouping element (C<< >> in the example) must not contain any +attributes or elements other than the grouped element. + +You can specify multiple 'grouping element' to 'grouped element' mappings in +the same hashref. If this option is combined with C, the array +folding will occur first and then the grouped element names will be eliminated. + +C will also use the grouptag mappings to re-introduce the tags around +the grouped elements. Beware though that this will occur in all places that +the 'grouping tag' name occurs - you probably don't want to use the same name +for elements as well as attributes. + +=head2 Handler => object_ref I<# out - SAX only> + +Use the 'Handler' option to have C generate SAX events rather than +returning a string of XML. For more details see L<"SAX SUPPORT"> below. + +Note: the current implementation of this option generates a string of XML +and uses a SAX parser to translate it into SAX events. The normal encoding +rules apply here - your data must be UTF8 encoded unless you specify an +alternative encoding via the 'XMLDecl' option; and by the time the data reaches +the handler object, it will be in UTF8 form regardless of the encoding you +supply. A future implementation of this option may generate the events +directly. + +=head2 KeepRoot => 1 I<# in+out - handy> + +In its attempt to return a data structure free of superfluous detail and +unnecessary levels of indirection, C normally discards the root +element name. Setting the 'KeepRoot' option to '1' will cause the root element +name to be retained. So after executing this code: + + $config = XMLin('', KeepRoot => 1) + +You'll be able to reference the tempdir as +C<$config-E{config}-E{tempdir}> instead of the default +C<$config-E{tempdir}>. + +Similarly, setting the 'KeepRoot' option to '1' will tell C that the +data structure already contains a root element name and it is not necessary to +add another. + +=head2 KeyAttr => [ list ] I<# in+out - important> + +This option controls the 'array folding' feature which translates nested +elements from an array to a hash. It also controls the 'unfolding' of hashes +to arrays. + +For example, this XML: + + + + + + +would, by default, parse to this: + + { + 'user' => [ + { + 'login' => 'grep', + 'fullname' => 'Gary R Epstein' + }, + { + 'login' => 'stty', + 'fullname' => 'Simon T Tyson' + } + ] + } + +If the option 'KeyAttr => "login"' were used to specify that the 'login' +attribute is a key, the same XML would parse to: + + { + 'user' => { + 'stty' => { + 'fullname' => 'Simon T Tyson' + }, + 'grep' => { + 'fullname' => 'Gary R Epstein' + } + } + } + +The key attribute names should be supplied in an arrayref if there is more +than one. C will attempt to match attribute names in the order +supplied. C will use the first attribute name supplied when +'unfolding' a hash into an array. + +Note 1: The default value for 'KeyAttr' is ['name', 'key', 'id']. If you do +not want folding on input or unfolding on output you must setting this option +to an empty list to disable the feature. + +Note 2: If you wish to use this option, you should also enable the +C option. Without 'ForceArray', a single nested element will be +rolled up into a scalar rather than an array and therefore will not be folded +(since only arrays get folded). + +=head2 KeyAttr => { list } I<# in+out - important> + +This alternative (and preferred) method of specifiying the key attributes +allows more fine grained control over which elements are folded and on which +attributes. For example the option 'KeyAttr => { package => 'id' } will cause +any package elements to be folded on the 'id' attribute. No other elements +which have an 'id' attribute will be folded at all. + +Note: C will generate a warning (or a fatal error in L<"STRICT MODE">) +if this syntax is used and an element which does not have the specified key +attribute is encountered (eg: a 'package' element without an 'id' attribute, to +use the example above). Warnings will only be generated if B<-w> is in force. + +Two further variations are made possible by prefixing a '+' or a '-' character +to the attribute name: + +The option 'KeyAttr => { user => "+login" }' will cause this XML: + + + + + + +to parse to this data structure: + + { + 'user' => { + 'stty' => { + 'fullname' => 'Simon T Tyson', + 'login' => 'stty' + }, + 'grep' => { + 'fullname' => 'Gary R Epstein', + 'login' => 'grep' + } + } + } + +The '+' indicates that the value of the key attribute should be copied rather +than moved to the folded hash key. + +A '-' prefix would produce this result: + + { + 'user' => { + 'stty' => { + 'fullname' => 'Simon T Tyson', + '-login' => 'stty' + }, + 'grep' => { + 'fullname' => 'Gary R Epstein', + '-login' => 'grep' + } + } + } + +As described earlier, C will ignore hash keys starting with a '-'. + +=head2 NoAttr => 1 I<# in+out - handy> + +When used with C, the generated XML will contain no attributes. +All hash key/values will be represented as nested elements instead. + +When used with C, any attributes in the XML will be ignored. + +=head2 NoEscape => 1 I<# out - seldom used> + +By default, C will translate the characters 'E', 'E', '&' and +'"' to '<', '>', '&' and '"' respectively. Use this option to +suppress escaping (presumably because you've already escaped the data in some +more sophisticated manner). + +=head2 NoIndent => 1 I<# out - seldom used> + +Set this option to 1 to disable C's default 'pretty printing' mode. +With this option enabled, the XML output will all be on one line (unless there +are newlines in the data) - this may be easier for downstream processing. + +=head2 NoSort => 1 I<# out - seldom used> + +Newer versions of XML::Simple sort elements and attributes alphabetically (*), +by default. Enable this option to suppress the sorting - possibly for +backwards compatibility. + +* Actually, sorting is alphabetical but 'key' attribute or element names (as in +'KeyAttr') sort first. Also, when a hash of hashes is 'unfolded', the elements +are sorted alphabetically by the value of the key field. + +=head2 NormaliseSpace => 0 | 1 | 2 I<# in - handy> + +This option controls how whitespace in text content is handled. Recognised +values for the option are: + +=over 4 + +=item * + +0 = (default) whitespace is passed through unaltered (except of course for the +normalisation of whitespace in attribute values which is mandated by the XML +recommendation) + +=item * + +1 = whitespace is normalised in any value used as a hash key (normalising means +removing leading and trailing whitespace and collapsing sequences of whitespace +characters to a single space) + +=item * + +2 = whitespace is normalised in all text content + +=back + +Note: you can spell this option with a 'z' if that is more natural for you. + +=head2 NSExpand => 1 I<# in+out handy - SAX only> + +This option controls namespace expansion - the translation of element and +attribute names of the form 'prefix:name' to '{uri}name'. For example the +element name 'xsl:template' might be expanded to: +'{http://www.w3.org/1999/XSL/Transform}template'. + +By default, C will return element names and attribute names exactly as +they appear in the XML. Setting this option to 1 will cause all element and +attribute names to be expanded to include their namespace prefix. + +I. + +This option also controls whether C performs the reverse translation +from '{uri}name' back to 'prefix:name'. The default is no translation. If +your data contains expanded names, you should set this option to 1 otherwise +C will emit XML which is not well formed. + +I to translate URIs back to prefixes>. + +=head2 NumericEscape => 0 | 1 | 2 I<# out - handy> + +Use this option to have 'high' (non-ASCII) characters in your Perl data +structure converted to numeric entities (eg: €) in the XML output. Three +levels are possible: + +0 - default: no numeric escaping (OK if you're writing out UTF8) + +1 - only characters above 0xFF are escaped (ie: characters in the 0x80-FF range are not escaped), possibly useful with ISO8859-1 output + +2 - all characters above 0x7F are escaped (good for plain ASCII output) + +=head2 OutputFile => I<# out - handy> + +The default behaviour of C is to return the XML as a string. If you +wish to write the XML to a file, simply supply the filename using the +'OutputFile' option. + +This option also accepts an IO handle object - especially useful in Perl 5.8.0 +and later for output using an encoding other than UTF-8, eg: + + open my $fh, '>:encoding(iso-8859-1)', $path or die "open($path): $!"; + XMLout($ref, OutputFile => $fh); + +Note, XML::Simple does not require that the object you pass in to the +OutputFile option inherits from L - it simply assumes the object +supports a C method. + +=head2 ParserOpts => [ XML::Parser Options ] I<# in - don't use this> + +I. + +This option allows you to pass parameters to the constructor of the underlying +XML::Parser object (which of course assumes you're not using SAX). + +=head2 RootName => 'string' I<# out - handy> + +By default, when C generates XML, the root element will be named +'opt'. This option allows you to specify an alternative name. + +Specifying either undef or the empty string for the RootName option will +produce XML with no root elements. In most cases the resulting XML fragment +will not be 'well formed' and therefore could not be read back in by C. +Nevertheless, the option has been found to be useful in certain circumstances. + +=head2 SearchPath => [ list ] I<# in - handy> + +If you pass C a filename, but the filename include no directory +component, you can use this option to specify which directories should be +searched to locate the file. You might use this option to search first in the +user's home directory, then in a global directory such as /etc. + +If a filename is provided to C but SearchPath is not defined, the +file is assumed to be in the current directory. + +If the first parameter to C is undefined, the default SearchPath +will contain only the directory in which the script itself is located. +Otherwise the default SearchPath will be empty. + +=head2 SuppressEmpty => 1 | '' | undef I<# in+out - handy> + +This option controls what C should do with empty elements (no +attributes and no content). The default behaviour is to represent them as +empty hashes. Setting this option to a true value (eg: 1) will cause empty +elements to be skipped altogether. Setting the option to 'undef' or the empty +string will cause empty elements to be represented as the undefined value or +the empty string respectively. The latter two alternatives are a little +easier to test for in your code than a hash with no keys. + +The option also controls what C does with undefined values. Setting +the option to undef causes undefined values to be output as empty elements +(rather than empty attributes), it also suppresses the generation of warnings +about undefined values. Setting the option to a true value (eg: 1) causes +undefined values to be skipped altogether on output. + +=head2 ValueAttr => [ names ] I<# in - handy> + +Use this option to deal elements which always have a single attribute and no +content. Eg: + + + + + + +Setting C<< ValueAttr => [ 'value' ] >> will cause the above XML to parse to: + + { + colour => 'red', + size => 'XXL' + } + +instead of this (the default): + + { + colour => { value => 'red' }, + size => { value => 'XXL' } + } + +Note: This form of the ValueAttr option is not compatible with C - +since the attribute name is discarded at parse time, the original XML cannot be +reconstructed. + +=head2 ValueAttr => { element => attribute, ... } I<# in+out - handy> + +This (preferred) form of the ValueAttr option requires you to specify both +the element and the attribute names. This is not only safer, it also allows +the original XML to be reconstructed by C. + +Note: You probably don't want to use this option and the NoAttr option at the +same time. + +=head2 Variables => { name => value } I<# in - handy> + +This option allows variables in the XML to be expanded when the file is read. +(there is no facility for putting the variable names back if you regenerate +XML using C). + +A 'variable' is any text of the form C<${name}> which occurs in an attribute +value or in the text content of an element. If 'name' matches a key in the +supplied hashref, C<${name}> will be replaced with the corresponding value from +the hashref. If no matching key is found, the variable will not be replaced. +Names must match the regex: C<[\w.]+> (ie: only 'word' characters and dots are +allowed). + +=head2 VarAttr => 'attr_name' I<# in - handy> + +In addition to the variables defined using C, this option allows +variables to be defined in the XML. A variable definition consists of an +element with an attribute called 'attr_name' (the value of the C +option). The value of the attribute will be used as the variable name and the +text content of the element will be used as the value. A variable defined in +this way will override a variable defined using the C option. For +example: + + XMLin( ' + /usr/local/apache + ${prefix} + ${exec_prefix}/bin + ', + VarAttr => 'name', ContentKey => '-content' + ); + +produces the following data structure: + + { + dir => { + prefix => '/usr/local/apache', + exec_prefix => '/usr/local/apache', + bindir => '/usr/local/apache/bin', + } + } + +=head2 XMLDecl => 1 or XMLDecl => 'string' I<# out - handy> + +If you want the output from C to start with the optional XML +declaration, simply set the option to '1'. The default XML declaration is: + + + +If you want some other string (for example to declare an encoding value), set +the value of this option to the complete string you require. + + +=head1 OPTIONAL OO INTERFACE + +The procedural interface is both simple and convenient however there are a +couple of reasons why you might prefer to use the object oriented (OO) +interface: + +=over 4 + +=item * + +to define a set of default values which should be used on all subsequent calls +to C or C + +=item * + +to override methods in B to provide customised behaviour + +=back + +The default values for the options described above are unlikely to suit +everyone. The OO interface allows you to effectively override B's +defaults with your preferred values. It works like this: + +First create an XML::Simple parser object with your preferred defaults: + + my $xs = XML::Simple->new(ForceArray => 1, KeepRoot => 1); + +then call C or C as a method of that object: + + my $ref = $xs->XMLin($xml); + my $xml = $xs->XMLout($ref); + +You can also specify options when you make the method calls and these values +will be merged with the values specified when the object was created. Values +specified in a method call take precedence. + +Note: when called as methods, the C and C routines may be +called as C or C. The method names are aliased so the +only difference is the aesthetics. + +=head2 Parsing Methods + +You can explicitly call one of the following methods rather than rely on the +C method automatically determining whether the target to be parsed is +a string, a file or a filehandle: + +=over 4 + +=item parse_string(text) + +Works exactly like the C method but assumes the first argument is +a string of XML (or a reference to a scalar containing a string of XML). + +=item parse_file(filename) + +Works exactly like the C method but assumes the first argument is +the name of a file containing XML. + +=item parse_fh(file_handle) + +Works exactly like the C method but assumes the first argument is +a filehandle which can be read to get XML. + +=back + +=head2 Hook Methods + +You can make your own class which inherits from XML::Simple and overrides +certain behaviours. The following methods may provide useful 'hooks' upon +which to hang your modified behaviour. You may find other undocumented methods +by examining the source, but those may be subject to change in future releases. + +=over 4 + +=item handle_options(direction, name => value ...) + +This method will be called when one of the parsing methods or the C +method is called. The initial argument will be a string (either 'in' or 'out') +and the remaining arguments will be name value pairs. + +=item default_config_file() + +Calculates and returns the name of the file which should be parsed if no +filename is passed to C (default: C<$0.xml>). + +=item build_simple_tree(filename, string) + +Called from C or any of the parsing methods. Takes either a file name +as the first argument or C followed by a 'string' as the second +argument. Returns a simple tree data structure. You could override this +method to apply your own transformations before the data structure is returned +to the caller. + +=item new_hashref() + +When the 'simple tree' data structure is being built, this method will be +called to create any required anonymous hashrefs. + +=item sorted_keys(name, hashref) + +Called when C is translating a hashref to XML. This routine returns +a list of hash keys in the order that the corresponding attributes/elements +should appear in the output. + +=item escape_value(string) + +Called from C, takes a string and returns a copy of the string with +XML character escaping rules applied. + +=item numeric_escape(string) + +Called from C, to handle non-ASCII characters (depending on the +value of the NumericEscape option). + +=item copy_hash(hashref, extra_key => value, ...) + +Called from C, when 'unfolding' a hash of hashes into an array of +hashes. You might wish to override this method if you're using tied hashes and +don't want them to get untied. + +=back + +=head2 Cache Methods + +XML::Simple implements three caching schemes ('storable', 'memshare' and +'memcopy'). You can implement a custom caching scheme by implementing +two methods - one for reading from the cache and one for writing to it. + +For example, you might implement a new 'dbm' scheme that stores cached data +structures using the L module. First, you would add a +C method which accepted a filename for use as a lookup key +and returned a data structure on success, or undef on failure. Then, you would +implement a C method which accepted a data structure and a +filename. + +You would use this caching scheme by specifying the option: + + Cache => [ 'dbm' ] + +=head1 STRICT MODE + +If you import the B routines like this: + + use XML::Simple qw(:strict); + +the following common mistakes will be detected and treated as fatal errors + +=over 4 + +=item * + +Failing to explicitly set the C option - if you can't be bothered +reading about this option, turn it off with: KeyAttr => [ ] + +=item * + +Failing to explicitly set the C option - if you can't be bothered +reading about this option, set it to the safest mode with: ForceArray => 1 + +=item * + +Setting ForceArray to an array, but failing to list all the elements from the +KeyAttr hash. + +=item * + +Data error - KeyAttr is set to say { part => 'partnum' } but the XML contains +one or more EpartE elements without a 'partnum' attribute (or nested +element). Note: if strict mode is not set but -w is, this condition triggers a +warning. + +=item * + +Data error - as above, but non-unique values are present in the key attribute +(eg: more than one EpartE element with the same partnum). This will +also trigger a warning if strict mode is not enabled. + +=item * + +Data error - as above, but value of key attribute (eg: partnum) is not a +scalar string (due to nested elements etc). This will also trigger a warning +if strict mode is not enabled. + +=back + +=head1 SAX SUPPORT + +From version 1.08_01, B includes support for SAX (the Simple API +for XML) - specifically SAX2. + +In a typical SAX application, an XML parser (or SAX 'driver') module generates +SAX events (start of element, character data, end of element, etc) as it parses +an XML document and a 'handler' module processes the events to extract the +required data. This simple model allows for some interesting and powerful +possibilities: + +=over 4 + +=item * + +Applications written to the SAX API can extract data from huge XML documents +without the memory overheads of a DOM or tree API. + +=item * + +The SAX API allows for plug and play interchange of parser modules without +having to change your code to fit a new module's API. A number of SAX parsers +are available with capabilities ranging from extreme portability to blazing +performance. + +=item * + +A SAX 'filter' module can implement both a handler interface for receiving +data and a generator interface for passing modified data on to a downstream +handler. Filters can be chained together in 'pipelines'. + +=item * + +One filter module might split a data stream to direct data to two or more +downstream handlers. + +=item * + +Generating SAX events is not the exclusive preserve of XML parsing modules. +For example, a module might extract data from a relational database using DBI +and pass it on to a SAX pipeline for filtering and formatting. + +=back + +B can operate at either end of a SAX pipeline. For example, +you can take a data structure in the form of a hashref and pass it into a +SAX pipeline using the 'Handler' option on C: + + use XML::Simple; + use Some::SAX::Filter; + use XML::SAX::Writer; + + my $ref = { + .... # your data here + }; + + my $writer = XML::SAX::Writer->new(); + my $filter = Some::SAX::Filter->new(Handler => $writer); + my $simple = XML::Simple->new(Handler => $filter); + $simple->XMLout($ref); + +You can also put B at the opposite end of the pipeline to take +advantage of the simple 'tree' data structure once the relevant data has been +isolated through filtering: + + use XML::SAX; + use Some::SAX::Filter; + use XML::Simple; + + my $simple = XML::Simple->new(ForceArray => 1, KeyAttr => ['partnum']); + my $filter = Some::SAX::Filter->new(Handler => $simple); + my $parser = XML::SAX::ParserFactory->parser(Handler => $filter); + + my $ref = $parser->parse_uri('some_huge_file.xml'); + + print $ref->{part}->{'555-1234'}; + +You can build a filter by using an XML::Simple object as a handler and setting +its DataHandler option to point to a routine which takes the resulting tree, +modifies it and sends it off as SAX events to a downstream handler: + + my $writer = XML::SAX::Writer->new(); + my $filter = XML::Simple->new( + DataHandler => sub { + my $simple = shift; + my $data = shift; + + # Modify $data here + + $simple->XMLout($data, Handler => $writer); + } + ); + my $parser = XML::SAX::ParserFactory->parser(Handler => $filter); + + $parser->parse_uri($filename); + +I but it could also have been specified in the constructor>. + +=head1 ENVIRONMENT + +If you don't care which parser module B uses then skip this +section entirely (it looks more complicated than it really is). + +B will default to using a B parser if one is available or +B if SAX is not available. + +You can dictate which parser module is used by setting either the environment +variable 'XML_SIMPLE_PREFERRED_PARSER' or the package variable +$XML::Simple::PREFERRED_PARSER to contain the module name. The following rules +are used: + +=over 4 + +=item * + +The package variable takes precedence over the environment variable if both are defined. To force B to ignore the environment settings and use +its default rules, you can set the package variable to an empty string. + +=item * + +If the 'preferred parser' is set to the string 'XML::Parser', then +L will be used (or C will die if L is not +installed). + +=item * + +If the 'preferred parser' is set to some other value, then it is assumed to be +the name of a SAX parser module and is passed to L +If L is not installed, or the requested parser module is not +installed, then C will die. + +=item * + +If the 'preferred parser' is not defined at all (the normal default +state), an attempt will be made to load L. If L is +installed, then a parser module will be selected according to +L's normal rules (which typically means the last SAX +parser installed). + +=item * + +if the 'preferred parser' is not defined and B is not +installed, then B will be used. C will die if +L is not installed. + +=back + +Note: The B distribution includes an XML parser written entirely in +Perl. It is very portable but it is not very fast. You should consider +installing L or L if they are available for your +platform. + +=head1 ERROR HANDLING + +The XML standard is very clear on the issue of non-compliant documents. An +error in parsing any single element (for example a missing end tag) must cause +the whole document to be rejected. B will die with an appropriate +message if it encounters a parsing error. + +If dying is not appropriate for your application, you should arrange to call +C in an eval block and look for errors in $@. eg: + + my $config = eval { XMLin() }; + PopUpMessage($@) if($@); + +Note, there is a common misconception that use of B will significantly +slow down a script. While that may be true when the code being eval'd is in a +string, it is not true of code like the sample above. + +=head1 EXAMPLES + +When C reads the following very simple piece of XML: + + + +it returns the following data structure: + + { + 'username' => 'testuser', + 'password' => 'frodo' + } + +The identical result could have been produced with this alternative XML: + + + +Or this (although see 'ForceArray' option for variations): + + + testuser + frodo + + +Repeated nested elements are represented as anonymous arrays: + + + + joe@smith.com + jsmith@yahoo.com + + + bob@smith.com + + + + { + 'person' => [ + { + 'email' => [ + 'joe@smith.com', + 'jsmith@yahoo.com' + ], + 'firstname' => 'Joe', + 'lastname' => 'Smith' + }, + { + 'email' => 'bob@smith.com', + 'firstname' => 'Bob', + 'lastname' => 'Smith' + } + ] + } + +Nested elements with a recognised key attribute are transformed (folded) from +an array into a hash keyed on the value of that attribute (see the C +option): + + + + + + + + { + 'person' => { + 'jbloggs' => { + 'firstname' => 'Joe', + 'lastname' => 'Bloggs' + }, + 'tsmith' => { + 'firstname' => 'Tom', + 'lastname' => 'Smith' + }, + 'jsmith' => { + 'firstname' => 'Joe', + 'lastname' => 'Smith' + } + } + } + + +The tag can be used to form anonymous arrays: + + + Col 1Col 2Col 3 + R1C1R1C2R1C3 + R2C1R2C2R2C3 + R3C1R3C2R3C3 + + + { + 'head' => [ + [ 'Col 1', 'Col 2', 'Col 3' ] + ], + 'data' => [ + [ 'R1C1', 'R1C2', 'R1C3' ], + [ 'R2C1', 'R2C2', 'R2C3' ], + [ 'R3C1', 'R3C2', 'R3C3' ] + ] + } + +Anonymous arrays can be nested to arbirtrary levels and as a special case, if +the surrounding tags for an XML document contain only an anonymous array the +arrayref will be returned directly rather than the usual hashref: + + + Col 1Col 2 + R1C1R1C2 + R2C1R2C2 + + + [ + [ 'Col 1', 'Col 2' ], + [ 'R1C1', 'R1C2' ], + [ 'R2C1', 'R2C2' ] + ] + +Elements which only contain text content will simply be represented as a +scalar. Where an element has both attributes and text content, the element +will be represented as a hashref with the text content in the 'content' key +(see the C option): + + + first + second + + + { + 'one' => 'first', + 'two' => { 'attr' => 'value', 'content' => 'second' } + } + +Mixed content (elements which contain both text content and nested elements) +will be not be represented in a useful way - element order and significant +whitespace will be lost. If you need to work with mixed content, then +XML::Simple is not the right tool for your job - check out the next section. + +=head1 WHERE TO FROM HERE? + +B is able to present a simple API because it makes some +assumptions on your behalf. These include: + +=over 4 + +=item * + +You're not interested in text content consisting only of whitespace + +=item * + +You don't mind that when things get slurped into a hash the order is lost + +=item * + +You don't want fine-grained control of the formatting of generated XML + +=item * + +You would never use a hash key that was not a legal XML element name + +=item * + +You don't need help converting between different encodings + +=back + +In a serious XML project, you'll probably outgrow these assumptions fairly +quickly. This section of the document used to offer some advice on chosing a +more powerful option. That advice has now grown into the 'Perl-XML FAQ' +document which you can find at: L + +The advice in the FAQ boils down to a quick explanation of tree versus +event based parsers and then recommends: + +For event based parsing, use SAX (do not set out to write any new code for +XML::Parser's handler API - it is obselete). + +For tree-based parsing, you could choose between the 'Perlish' approach of +L and more standards based DOM implementations - preferably one with +XPath support. + + +=head1 SEE ALSO + +B requires either L or L. + +To generate documents with namespaces, L is required. + +The optional caching functions require L. + +Answers to Frequently Asked Questions about XML::Simple are bundled with this +distribution as: L + +=head1 COPYRIGHT + +Copyright 1999-2004 Grant McLean Egrantm@cpan.orgE + +This library is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +=cut + + diff --git a/share/perl/lib/XML/TreePP.pm b/share/perl/lib/XML/TreePP.pm new file mode 100644 index 0000000..bd03db7 --- /dev/null +++ b/share/perl/lib/XML/TreePP.pm @@ -0,0 +1,1228 @@ +=head1 NAME + +XML::TreePP -- Pure Perl implementation for parsing/writing xml files + +=head1 SYNOPSIS + +parse xml file into hash tree + + use XML::TreePP; + my $tpp = XML::TreePP->new(); + my $tree = $tpp->parsefile( "index.rdf" ); + print "Title: ", $tree->{"rdf:RDF"}->{item}->[0]->{title}, "\n"; + print "URL: ", $tree->{"rdf:RDF"}->{item}->[0]->{link}, "\n"; + +write xml as string from hash tree + + use XML::TreePP; + my $tpp = XML::TreePP->new(); + my $tree = { rss => { channel => { item => [ { + title => "The Perl Directory", + link => "http://www.perl.org/", + }, { + title => "The Comprehensive Perl Archive Network", + link => "http://cpan.perl.org/", + } ] } } }; + my $xml = $tpp->write( $tree ); + print $xml; + +get remote xml file with HTTP-GET and parse it into hash tree + + use XML::TreePP; + my $tpp = XML::TreePP->new(); + my $tree = $tpp->parsehttp( GET => "http://use.perl.org/index.rss" ); + print "Title: ", $tree->{"rdf:RDF"}->{channel}->{title}, "\n"; + print "URL: ", $tree->{"rdf:RDF"}->{channel}->{link}, "\n"; + +get remote xml file with HTTP-POST and parse it into hash tree + + use XML::TreePP; + my $tpp = XML::TreePP->new( force_array => [qw( item )] ); + my $cgiurl = "http://search.hatena.ne.jp/keyword"; + my $keyword = "ajax"; + my $cgiquery = "mode=rss2&word=".$keyword; + my $tree = $tpp->parsehttp( POST => $cgiurl, $cgiquery ); + print "Link: ", $tree->{rss}->{channel}->{item}->[0]->{link}, "\n"; + print "Desc: ", $tree->{rss}->{channel}->{item}->[0]->{description}, "\n"; + +=head1 DESCRIPTION + +XML::TreePP module parses XML file and expands it for a hash tree. +And also generate XML file from a hash tree. +This is a pure Perl implementation. +You can also download XML from remote web server +like XMLHttpRequest object at JavaScript language. + +=head1 EXAMPLES + +=head2 Parse XML file + +Sample XML source: + + + + Yasuhisa + Chizuko + + Shiori + Yusuke + Kairi + + + +Sample program to read a xml file and dump it: + + use XML::TreePP; + use Data::Dumper; + my $tpp = XML::TreePP->new(); + my $tree = $tpp->parsefile( "family.xml" ); + my $text = Dumper( $tree ); + print $text; + +Result dumped: + + $VAR1 = { + 'family' => { + '-name' => 'Kawasaki', + 'father' => 'Yasuhisa', + 'mother' => 'Chizuko', + 'children' => { + 'girl' => 'Shiori' + 'boy' => [ + 'Yusuke', + 'Kairi' + ], + } + } + }; + +Details: + + print $tree->{family}->{father}; # the father's given name. + +The prefix '-' is added on every attribute's name. + + print $tree->{family}->{"-name"}; # the family name of the family + +The array is used because the family has two boys. + + print $tree->{family}->{children}->{boy}->[1]; # The second boy's name + print $tree->{family}->{children}->{girl}; # The girl's name + +=head2 Text node and attributes: + +If a element has both of a text node and attributes +or both of a text node and other child nodes, +value of a text node is moved to C<#text> like child nodes. + + use XML::TreePP; + use Data::Dumper; + my $tpp = XML::TreePP->new(); + my $source = 'Kawasaki Yusuke'; + my $tree = $tpp->parse( $source ); + my $text = Dumper( $tree ); + print $text; + +The result dumped is following: + + $VAR1 = { + 'span' => { + '-class' => 'author', + '#text' => 'Kawasaki Yusuke' + } + }; + +The special node name of C<#text> is used because this elements +has attribute(s) in addition to the text node. +See also L option. + +=head1 METHODS + +=head2 new + +This constructor method returns a new XML::TreePP object with C<%options>. + + $tpp = XML::TreePP->new( %options ); + +=head2 set + +This method sets a option value for C. +If C<$option_value> is not defined, its option is deleted. + + $tpp->set( option_name => $option_value ); + +See OPTIONS section below for details. + +=head2 get + +This method returns a current option value for C. + + $tpp->get( 'option_name' ); + +=head2 parse + +This method reads XML source and returns a hash tree converted. +The first argument is a scalar or a reference to a scalar. + + $tree = $tpp->parse( $source ); + +=head2 parsefile + +This method reads a XML file and returns a hash tree converted. +The first argument is a filename. + + $tree = $tpp->parsefile( $file ); + +=head2 parsehttp + +This method receives a XML file from a remote server via HTTP and +returns a hash tree converted. + + $tree = $tpp->parsehttp( $method, $url, $body, $head ); + +C<$method> is a method of HTTP connection: GET/POST/PUT/DELETE +C<$url> is an URI of a XML file. +C<$body> is a request body when you use POST method. +C<$head> is a request headers as a hash ref. +L module or L module is required to fetch a file. + + ( $tree, $xml, $code ) = $tpp->parsehttp( $method, $url, $body, $head ); + +In array context, This method returns also raw XML source received +and HTTP response's status code. + +=head2 write + +This method parses a hash tree and returns a XML source generated. + + $source = $tpp->write( $tree, $encode ); + +C<$tree> is a reference to a hash tree. + +=head2 writefile + +This method parses a hash tree and writes a XML source into a file. + + $tpp->writefile( $file, $tree, $encode ); + +C<$file> is a filename to create. +C<$tree> is a reference to a hash tree. + +=head1 OPTIONS FOR PARSING XML + +This module accepts option parameters following: + +=head2 force_array + +This option allows you to specify a list of element names which +should always be forced into an array representation. + + $tpp->set( force_array => [ 'rdf:li', 'item', '-xmlns' ] ); + +The default value is null, it means that context of the elements +will determine to make array or to keep it scalar or hash. +Note that the special wildcard name C<'*'> means all elements. + +=head2 force_hash + +This option allows you to specify a list of element names which +should always be forced into an hash representation. + + $tpp->set( force_hash => [ 'item', 'image' ] ); + +The default value is null, it means that context of the elements +will determine to make hash or to keep it scalar as a text node. +See also L option below. +Note that the special wildcard name C<'*'> means all elements. + +=head2 cdata_scalar_ref + +This option allows you to convert a cdata section into a reference +for scalar on parsing XML source. + + $tpp->set( cdata_scalar_ref => 1 ); + +The default value is false, it means that each cdata section is converted into a scalar. + +=head2 user_agent + +This option allows you to specify a HTTP_USER_AGENT string which +is used by parsehttp() method. + + $tpp->set( user_agent => 'Mozilla/4.0 (compatible; ...)' ); + +The default string is C<'XML-TreePP/#.##'>, where C<'#.##'> is +substituted with the version number of this library. + +=head2 http_lite + +This option forces pasrsehttp() method to use a L instance. + + my $http = HTTP::Lite->new(); + $tpp->set( http_lite => $http ); + +=head2 lwp_useragent + +This option forces pasrsehttp() method to use a L instance. + + my $ua = LWP::UserAgent->new(); + $ua->timeout( 60 ); + $ua->env_proxy; + $tpp->set( lwp_useragent => $ua ); + +You may use this with L. + +=head2 base_class + +This blesses class name for each element's hashref. +Each class is named straight as a child class of it parent class. + + $tpp->set( base_class => 'MyElement' ); + my $xml = 'text'; + my $tree = $tpp->parse( $xml ); + print ref $tree->{root}->{parent}->{child}, "\n"; + +A hash for element above is blessed to C +class. You may use this with L. + +=head2 elem_class + +This blesses class name for each element's hashref. +Each class is named horizontally under the direct child of C. + + $tpp->set( base_class => 'MyElement' ); + my $xml = 'text'; + my $tree = $tpp->parse( $xml ); + print ref $tree->{root}->{parent}->{child}, "\n"; + +A hash for element above is blessed to C class. + +=head1 OPTIONS FOR WRITING XML + +=head2 first_out + +This option allows you to specify a list of element/attribute +names which should always appears at first on output XML code. + + $tpp->set( first_out => [ 'link', 'title', '-type' ] ); + +The default value is null, it means alphabetical order is used. + +=head2 last_out + +This option allows you to specify a list of element/attribute +names which should always appears at last on output XML code. + + $tpp->set( last_out => [ 'items', 'item', 'entry' ] ); + +=head2 indent + +This makes the output more human readable by indenting appropriately. + + $tpp->set( indent => 2 ); + +This doesn't strictly follow the XML Document Spec but does looks nice. + +=head2 xml_decl + +This module generates an XML declaration on writing an XML code per default. +This option forces to change or leave it. + + $tpp->set( xml_decl => '' ); + +=head2 output_encoding + +This option allows you to specify a encoding of xml file generated +by write/writefile methods. + + $tpp->set( output_encoding => 'UTF-8' ); + +On Perl 5.8.0 and later, you can select it from every +encodings supported by Encode.pm. On Perl 5.6.x and before with +Jcode.pm, you can use C, C, C and +C. The default value is C which is recommended encoding. + +=head1 OPTIONS FOR BOTH + +=head2 utf8_flag + +This makes utf8 flag on for every element's value parsed +and makes it on for an XML code generated as well. + + $tpp->set( utf8_flag => 1 ); + +Perl 5.8.1 or later is required to use this. + +=head2 attr_prefix + +This option allows you to specify a prefix character(s) which +is inserted before each attribute names. + + $tpp->set( attr_prefix => '@' ); + +The default character is C<'-'>. +Or set C<'@'> to access attribute values like E4X, ECMAScript for XML. +Zero-length prefix C<''> is available as well, it means no prefix is added. + +=head2 text_node_key + +This option allows you to specify a hash key for text nodes. + + $tpp->set( text_node_key => '#text' ); + +The default key is C<#text>. + +=head2 ignore_error + +This module calls Carp::croak function on an error per default. +This option makes all errors ignored and just return. + + $tpp->set( ignore_error => 1 ); + +=head2 use_ixhash + +This option keeps the order for each element appeared in XML. +L module is required. + + $tpp->set( use_ixhash => 1 ); + +This makes parsing performance slow. +(about 100% slower than default) + +=head1 AUTHOR + +Yusuke Kawasaki, http://www.kawa.net/ + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 2006-2007 Yusuke Kawasaki. All rights reserved. +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +=cut + +package XML::TreePP; +use strict; +use Carp; +use Symbol; + +use vars qw( $VERSION ); +$VERSION = '0.32'; + +my $XML_ENCODING = 'UTF-8'; +my $INTERNAL_ENCODING = 'UTF-8'; +my $USER_AGENT = 'XML-TreePP/'.$VERSION.' '; +my $ATTR_PREFIX = '-'; +my $TEXT_NODE_KEY = '#text'; + +sub new { + my $package = shift; + my $self = {@_}; + bless $self, $package; + $self; +} + +sub die { + my $self = shift; + my $mess = shift; + return if $self->{ignore_error}; + Carp::croak $mess; +} + +sub warn { + my $self = shift; + my $mess = shift; + return if $self->{ignore_error}; + Carp::carp $mess; +} + +sub set { + my $self = shift; + my $key = shift; + my $val = shift; + if ( defined $val ) { + $self->{$key} = $val; + } + else { + delete $self->{$key}; + } +} + +sub get { + my $self = shift; + my $key = shift; + $self->{$key} if exists $self->{$key}; +} + +sub writefile { + my $self = shift; + my $file = shift; + my $tree = shift or return $self->die( 'Invalid tree' ); + my $encode = shift; + return $self->die( 'Invalid filename' ) unless defined $file; + my $text = $self->write( $tree, $encode ); + if ( $] >= 5.008001 && utf8::is_utf8( $text ) ) { + utf8::encode( $text ); + } + $self->write_raw_xml( $file, $text ); +} + +sub write { + my $self = shift; + my $tree = shift or return $self->die( 'Invalid tree' ); + my $from = $self->{internal_encoding} || $INTERNAL_ENCODING; + my $to = shift || $self->{output_encoding} || $XML_ENCODING; + my $decl = $self->{xml_decl}; + $decl = '' unless defined $decl; + + local $self->{__first_out}; + if ( exists $self->{first_out} ) { + my $keys = $self->{first_out}; + $keys = [$keys] unless ref $keys; + $self->{__first_out} = { map { $keys->[$_] => $_ } 0 .. $#$keys }; + } + + local $self->{__last_out}; + if ( exists $self->{last_out} ) { + my $keys = $self->{last_out}; + $keys = [$keys] unless ref $keys; + $self->{__last_out} = { map { $keys->[$_] => $_ } 0 .. $#$keys }; + } + + my $tnk = $self->{text_node_key} if exists $self->{text_node_key}; + $tnk = $TEXT_NODE_KEY unless defined $tnk; + local $self->{text_node_key} = $tnk; + + my $apre = $self->{attr_prefix} if exists $self->{attr_prefix}; + $apre = $ATTR_PREFIX unless defined $apre; + local $self->{__attr_prefix_len} = length($apre); + local $self->{__attr_prefix_rex} = defined $apre ? qr/^\Q$apre\E/s : undef; + + local $self->{__indent}; + if ( exists $self->{indent} && $self->{indent} ) { + $self->{__indent} = ' ' x $self->{indent}; + } + + my $text = $self->hash_to_xml( undef, $tree ); + if ( $from && $to ) { + my $stat = $self->encode_from_to( \$text, $from, $to ); + return $self->die( "Unsupported encoding: $to" ) unless $stat; + } + + return $text if ( $decl eq '' ); + join( "\n", $decl, $text ); +} + +sub parsehttp { + my $self = shift; + + local $self->{__user_agent}; + if ( exists $self->{user_agent} ) { + my $agent = $self->{user_agent}; + $agent .= $USER_AGENT if ( $agent =~ /\s$/s ); + $self->{__user_agent} = $agent if ( $agent ne '' ); + } else { + $self->{__user_agent} = $USER_AGENT; + } + + my $http = $self->{__http_module}; + unless ( $http ) { + $http = $self->find_http_module(@_); + $self->{__http_module} = $http; + } + if ( $http eq 'LWP::UserAgent' ) { + return $self->parsehttp_lwp(@_); + } + elsif ( $http eq 'HTTP::Lite' ) { + return $self->parsehttp_lite(@_); + } + else { + return $self->die( "LWP::UserAgent or HTTP::Lite is required: $_[1]" ); + } +} + +sub find_http_module { + my $self = shift || {}; + + if ( exists $self->{lwp_useragent} && ref $self->{lwp_useragent} ) { + return 'LWP::UserAgent' if defined $LWP::UserAgent::VERSION; + return 'LWP::UserAgent' if &load_lwp_useragent(); + return $self->die( "LWP::UserAgent is required: $_[1]" ); + } + + if ( exists $self->{http_lite} && ref $self->{http_lite} ) { + return 'HTTP::Lite' if defined $HTTP::Lite::VERSION; + return 'HTTP::Lite' if &load_http_lite(); + return $self->die( "HTTP::Lite is required: $_[1]" ); + } + + return 'LWP::UserAgent' if defined $LWP::UserAgent::VERSION; + return 'HTTP::Lite' if defined $HTTP::Lite::VERSION; + return 'LWP::UserAgent' if &load_lwp_useragent(); + return 'HTTP::Lite' if &load_http_lite(); + return $self->die( "LWP::UserAgent or HTTP::Lite is required: $_[1]" ); +} + +sub load_lwp_useragent { + return $LWP::UserAgent::VERSION if defined $LWP::UserAgent::VERSION; + local $@; + eval { require LWP::UserAgent; }; + $LWP::UserAgent::VERSION; +} + +sub load_http_lite { + return $HTTP::Lite::VERSION if defined $HTTP::Lite::VERSION; + local $@; + eval { require HTTP::Lite; }; + $HTTP::Lite::VERSION; +} + +sub load_tie_ixhash { + return $Tie::IxHash::VERSION if defined $Tie::IxHash::VERSION; + local $@; + eval { require Tie::IxHash; }; + $Tie::IxHash::VERSION; +} + +sub parsehttp_lwp { + my $self = shift; + my $method = shift or return $self->die( 'Invalid HTTP method' ); + my $url = shift or return $self->die( 'Invalid URL' ); + my $body = shift; + my $header = shift; + + my $ua = $self->{lwp_useragent} if exists $self->{lwp_useragent}; + if ( ! ref $ua ) { + $ua = LWP::UserAgent->new(); + $ua->timeout(10); + $ua->env_proxy(); + $ua->agent( $self->{__user_agent} ) if defined $self->{__user_agent}; + } else { + $ua->agent( $self->{__user_agent} ) if exists $self->{user_agent}; + } + + my $req = HTTP::Request->new( $method, $url ); + my $ct = 0; + if ( ref $header ) { + foreach my $field ( sort keys %$header ) { + my $value = $header->{$field}; + $req->header( $field => $value ); + $ct ++ if ( $field =~ /^Content-Type$/i ); + } + } + if ( defined $body && ! $ct ) { + $req->header( 'Content-Type' => 'application/x-www-form-urlencoded' ); + } + $req->content($body) if defined $body; + my $res = $ua->request($req); + my $code = $res->code(); + my $text = $res->content(); + my $tree = $self->parse( \$text ) if $res->is_success(); + wantarray ? ( $tree, $text, $code ) : $tree; +} + +sub parsehttp_lite { + my $self = shift; + my $method = shift or return $self->die( 'Invalid HTTP method' ); + my $url = shift or return $self->die( 'Invalid URL' ); + my $body = shift; + my $header = shift; + + my $http = HTTP::Lite->new(); + $http->method($method); + my $ua = 0; + if ( ref $header ) { + foreach my $field ( sort keys %$header ) { + my $value = $header->{$field}; + $http->add_req_header( $field, $value ); + $ua ++ if ( $field =~ /^User-Agent$/i ); + } + } + if ( defined $self->{__user_agent} && ! $ua ) { + $http->add_req_header( 'User-Agent', $self->{__user_agent} ); + } + $http->{content} = $body if defined $body; + my $code = $http->request($url) or return; + my $text = $http->body(); + my $tree = $self->parse( \$text ); + wantarray ? ( $tree, $text, $code ) : $tree; +} + +sub parsefile { + my $self = shift; + my $file = shift; + return $self->die( 'Invalid filename' ) unless defined $file; + my $text = $self->read_raw_xml($file); + $self->parse( \$text ); +} + +sub parse { + my $self = shift; + my $text = ref $_[0] ? ${$_[0]} : $_[0]; + return $self->die( 'Null XML source' ) unless defined $text; + + my $from = &xml_decl_encoding(\$text) || $XML_ENCODING; + my $to = $self->{internal_encoding} || $INTERNAL_ENCODING; + if ( $from && $to ) { + my $stat = $self->encode_from_to( \$text, $from, $to ); + return $self->die( "Unsupported encoding: $from" ) unless $stat; + } + + local $self->{__force_array}; + local $self->{__force_array_all}; + if ( exists $self->{force_array} ) { + my $force = $self->{force_array}; + $force = [$force] unless ref $force; + $self->{__force_array} = { map { $_ => 1 } @$force }; + $self->{__force_array_all} = $self->{__force_array}->{'*'}; + } + + local $self->{__force_hash}; + local $self->{__force_hash_all}; + if ( exists $self->{force_hash} ) { + my $force = $self->{force_hash}; + $force = [$force] unless ref $force; + $self->{__force_hash} = { map { $_ => 1 } @$force }; + $self->{__force_hash_all} = $self->{__force_hash}->{'*'}; + } + + my $tnk = $self->{text_node_key} if exists $self->{text_node_key}; + $tnk = $TEXT_NODE_KEY unless defined $tnk; + local $self->{text_node_key} = $tnk; + + my $apre = $self->{attr_prefix} if exists $self->{attr_prefix}; + $apre = $ATTR_PREFIX unless defined $apre; + local $self->{attr_prefix} = $apre; + + if ( exists $self->{use_ixhash} && $self->{use_ixhash} ) { + return $self->die( "Tie::IxHash is required." ) unless &load_tie_ixhash(); + } + + my $flat = $self->xml_to_flat(\$text); + my $class = $self->{base_class} if exists $self->{base_class}; + my $tree = $self->flat_to_tree( $flat, '', $class ); + if ( ref $tree ) { + if ( defined $class ) { + bless( $tree, $class ); + } + elsif ( exists $self->{elem_class} && $self->{elem_class} ) { + bless( $tree, $self->{elem_class} ); + } + } + wantarray ? ( $tree, $text ) : $tree; +} + +sub xml_to_flat { + my $self = shift; + my $textref = shift; # reference + my $flat = []; + my $prefix = $self->{attr_prefix}; + my $ixhash = ( exists $self->{use_ixhash} && $self->{use_ixhash} ); + + while ( $$textref =~ m{ + ([^<]*) < + (( + \? ([^<>]*) \? + )|( + \!\[CDATA\[(.*?)\]\] + )|( + \!DOCTYPE\s+([^\[\]<>]*(?:\[.*?\]\s*)?) + )|( + \!--(.*?)-- + )|( + ([^\!\?\s<>](?:"[^"]*"|'[^']*'|[^"'<>])*) + )) + > ([^<]*) + }sxg ) { + my ( + $ahead, $match, $typePI, $contPI, $typeCDATA, + $contCDATA, $typeDocT, $contDocT, $typeCmnt, $contCmnt, + $typeElem, $contElem, $follow + ) + = ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ); + if ( defined $ahead && $ahead =~ /\S/ ) { + $self->warn( "Invalid string: [$ahead] before <$match>" ); + } + + if ($typeElem) { # Element + my $node = {}; + if ( $contElem =~ s#^/## ) { + $node->{endTag}++; + } + elsif ( $contElem =~ s#/$## ) { + $node->{emptyTag}++; + } + else { + $node->{startTag}++; + } + $node->{tagName} = $1 if ( $contElem =~ s#^(\S+)\s*## ); + unless ( $node->{endTag} ) { + my $attr; + while ( $contElem =~ m{ + ([^\s\=\"\']+)=(?:(")(.*?)"|'(.*?)') + }sxg ) { + my $key = $1; + my $val = &xml_unescape( $2 ? $3 : $4 ); + if ( ! ref $attr ) { + $attr = {}; + tie( %$attr, 'Tie::IxHash' ) if $ixhash; + } + $attr->{$prefix.$key} = $val; + } + $node->{attributes} = $attr if ref $attr; + } + push( @$flat, $node ); + } + elsif ($typeCDATA) { ## CDATASection + if ( exists $self->{cdata_scalar_ref} && $self->{cdata_scalar_ref} ) { + push( @$flat, \$contCDATA ); # as reference for scalar + } + else { + push( @$flat, $contCDATA ); # as scalar like text node + } + } + elsif ($typeCmnt) { # Comment (ignore) + } + elsif ($typeDocT) { # DocumentType (ignore) + } + elsif ($typePI) { # ProcessingInstruction (ignore) + } + else { + $self->warn( "Invalid Tag: <$match>" ); + } + if ( $follow =~ /\S/ ) { # text node + my $val = &xml_unescape($follow); + push( @$flat, $val ); + } + } + $flat; +} + +sub flat_to_tree { + my $self = shift; + my $source = shift; + my $parent = shift; + my $class = shift; + my $tree = {}; + my $text = []; + + if ( exists $self->{use_ixhash} && $self->{use_ixhash} ) { + tie( %$tree, 'Tie::IxHash' ); + } + + while ( scalar @$source ) { + my $node = shift @$source; + if ( !ref $node || UNIVERSAL::isa( $node, "SCALAR" ) ) { + push( @$text, $node ); # cdata or text node + next; + } + my $name = $node->{tagName}; + if ( $node->{endTag} ) { + last if ( $parent eq $name ); + return $self->die( "Invalid tag sequence: <$parent>" ); + } + my $elem = $node->{attributes}; + my $forcehash = $self->{__force_hash_all} || $self->{__force_hash}->{$name}; + my $subclass; + if ( defined $class ) { + my $escname = $name; + $escname =~ s/\W/_/sg; + $subclass = $class.'::'.$escname; + } + if ( $node->{startTag} ) { # recursive call + my $child = $self->flat_to_tree( $source, $name, $subclass ); + next unless defined $child; + my $hasattr = scalar keys %$elem if ref $elem; + if ( UNIVERSAL::isa( $child, "HASH" ) ) { + if ( $hasattr ) { + # some attributes and some child nodes + %$elem = ( %$elem, %$child ); + } + else { + # some child nodes without attributes + $elem = $child; + } + } + else { + if ( $hasattr ) { + # some attributes and text node + $elem->{$self->{text_node_key}} = $child; + } + elsif ( $forcehash ) { + # only text node without attributes + $elem = { $self->{text_node_key} => $child }; + } + else { + # text node without attributes + $elem = $child; + } + } + } + elsif ( $forcehash && ! ref $elem ) { + $elem = {}; + } + # bless to a class by base_class or elem_class + if ( ref $elem && UNIVERSAL::isa( $elem, "HASH" ) ) { + if ( defined $subclass ) { + bless( $elem, $subclass ); + } elsif ( exists $self->{elem_class} && $self->{elem_class} ) { + my $escname = $name; + $escname =~ s/\W/_/sg; + my $elmclass = $self->{elem_class}.'::'.$escname; + bless( $elem, $elmclass ); + } + } + # next unless defined $elem; + $tree->{$name} ||= []; + push( @{ $tree->{$name} }, $elem ); + } + if ( ! $self->{__force_array_all} ) { + foreach my $key ( keys %$tree ) { + next if $self->{__force_array}->{$key}; + next if ( 1 < scalar @{ $tree->{$key} } ); + $tree->{$key} = shift @{ $tree->{$key} }; + } + } + my $haschild = scalar keys %$tree; + if ( scalar @$text ) { + if ( scalar @$text == 1 ) { + # one text node (normal) + $text = shift @$text; + } + elsif ( ! scalar grep {ref $_} @$text ) { + # some text node splitted + $text = join( '', @$text ); + } + else { + # some cdata node + my $join = join( '', map {ref $_ ? $$_ : $_} @$text ); + $text = \$join; + } + if ( $haschild ) { + # some child nodes and also text node + $tree->{$self->{text_node_key}} = $text; + } + else { + # only text node without child nodes + $tree = $text; + } + } + elsif ( ! $haschild ) { + # no child and no text + $tree = ""; + } + $tree; +} + +sub hash_to_xml { + my $self = shift; + my $name = shift; + my $hash = shift; + my $out = []; + my $attr = []; + my $allkeys = [ keys %$hash ]; + my $fo = $self->{__first_out} if ref $self->{__first_out}; + my $lo = $self->{__last_out} if ref $self->{__last_out}; + my $firstkeys = [ sort { $fo->{$a} <=> $fo->{$b} } grep { exists $fo->{$_} } @$allkeys ] if ref $fo; + my $lastkeys = [ sort { $lo->{$a} <=> $lo->{$b} } grep { exists $lo->{$_} } @$allkeys ] if ref $lo; + $allkeys = [ grep { ! exists $fo->{$_} } @$allkeys ] if ref $fo; + $allkeys = [ grep { ! exists $lo->{$_} } @$allkeys ] if ref $lo; + unless ( exists $self->{use_ixhash} && $self->{use_ixhash} ) { + $allkeys = [ sort @$allkeys ]; + } + my $prelen = $self->{__attr_prefix_len}; + my $pregex = $self->{__attr_prefix_rex}; + + foreach my $keys ( $firstkeys, $allkeys, $lastkeys ) { + next unless ref $keys; + my $elemkey = $prelen ? [ grep { $_ !~ $pregex } @$keys ] : $keys; + my $attrkey = $prelen ? [ grep { $_ =~ $pregex } @$keys ] : []; + + foreach my $key ( @$elemkey ) { + my $val = $hash->{$key}; + if ( !defined $val ) { + push( @$out, "<$key />" ); + } + elsif ( UNIVERSAL::isa( $val, 'ARRAY' ) ) { + my $child = $self->array_to_xml( $key, $val ); + push( @$out, $child ); + } + elsif ( UNIVERSAL::isa( $val, 'SCALAR' ) ) { + my $child = $self->scalaref_to_cdata( $key, $val ); + push( @$out, $child ); + } + elsif ( ref $val ) { + my $child = $self->hash_to_xml( $key, $val ); + push( @$out, $child ); + } + else { + my $child = $self->scalar_to_xml( $key, $val ); + push( @$out, $child ); + } + } + + foreach my $key ( @$attrkey ) { + my $name = substr( $key, $prelen ); + my $val = &xml_escape( $hash->{$key} ); + push( @$attr, ' ' . $name . '="' . $val . '"' ); + } + } + my $jattr = join( '', @$attr ); + + if ( defined $name && scalar @$out && ! grep { ! /^{__indent} ) { + s/^(\s*<)/$self->{__indent}$1/mg foreach @$out; + } + unshift( @$out, "\n" ); + } + + my $text = join( '', @$out ); + if ( defined $name ) { + if ( scalar @$out ) { + $text = "<$name$jattr>$text\n"; + } + else { + $text = "<$name$jattr />\n"; + } + } + $text; +} + +sub array_to_xml { + my $self = shift; + my $name = shift; + my $array = shift; + my $out = []; + foreach my $val (@$array) { + if ( !defined $val ) { + push( @$out, "<$name />\n" ); + } + elsif ( UNIVERSAL::isa( $val, 'ARRAY' ) ) { + my $child = $self->array_to_xml( $name, $val ); + push( @$out, $child ); + } + elsif ( UNIVERSAL::isa( $val, 'SCALAR' ) ) { + my $child = $self->scalaref_to_cdata( $name, $val ); + push( @$out, $child ); + } + elsif ( ref $val ) { + my $child = $self->hash_to_xml( $name, $val ); + push( @$out, $child ); + } + else { + my $child = $self->scalar_to_xml( $name, $val ); + push( @$out, $child ); + } + } + + my $text = join( '', @$out ); + $text; +} + +sub scalaref_to_cdata { + my $self = shift; + my $name = shift; + my $ref = shift; + my $data = defined $$ref ? $$ref : ''; + $data =~ s#(]])(>)#$1]]>'; + my $text = $data; + $text = "<$name>$text\n" if ( $name ne $self->{text_node_key} ); + $text; +} + +sub scalar_to_xml { + my $self = shift; + my $name = shift; + my $scalar = shift; + my $copy = $scalar; + my $text = &xml_escape($copy); + $text = "<$name>$text\n" if ( $name ne $self->{text_node_key} ); + $text; +} + +sub write_raw_xml { + my $self = shift; + my $file = shift; + my $fh = Symbol::gensym(); + open( $fh, ">$file" ) or return $self->die( "$! - $file" ); + print $fh @_; + close($fh); +} + +sub read_raw_xml { + my $self = shift; + my $file = shift; + my $fh = Symbol::gensym(); + open( $fh, $file ) or return $self->die( "$! - $file" ); + local $/ = undef; + my $text = <$fh>; + close($fh); + $text; +} + +sub xml_decl_encoding { + my $textref = shift; + return unless defined $$textref; + my $args = ( $$textref =~ /^\s*<\?xml(\s+\S.*)\?>/s )[0] or return; + my $getcode = ( $args =~ /\s+encoding=(".*?"|'.*?')/ )[0] or return; + $getcode =~ s/^['"]//; + $getcode =~ s/['"]$//; + $getcode; +} + +sub encode_from_to { + my $self = shift; + my $txtref = shift or return; + my $from = shift or return; + my $to = shift or return; + + unless ( defined $Encode::EUCJPMS::VERSION ) { + $from = 'EUC-JP' if ( $from =~ /\beuc-?jp-?(win|ms)$/i ); + $to = 'EUC-JP' if ( $to =~ /\beuc-?jp-?(win|ms)$/i ); + } + + my $setflag = $self->{utf8_flag} if exists $self->{utf8_flag}; + if ( $] < 5.008001 && $setflag ) { + return $self->die( "Perl 5.8.1 is required for utf8_flag: $]" ); + } + + if ( $] >= 5.008 ) { + &load_encode(); + my $check = ( $Encode::VERSION < 2.13 ) ? 0x400 : Encode::FB_XMLCREF(); + if ( $] >= 5.008001 && utf8::is_utf8( $$txtref ) ) { + if ( $to =~ /^utf-?8$/i ) { + # skip + } else { + $$txtref = Encode::encode( $to, $$txtref, $check ); + } + } else { + $$txtref = Encode::decode( $from, $$txtref ); + if ( $to =~ /^utf-?8$/i && $setflag ) { + # skip + } else { + $$txtref = Encode::encode( $to, $$txtref, $check ); + } + } + } + elsif ( ( uc($from) eq 'ISO-8859-1' + || uc($from) eq 'US-ASCII' + || uc($from) eq 'LATIN-1' ) && uc($to) eq 'UTF-8' ) { + &latin1_to_utf8($txtref); + } + else { + my $jfrom = &get_jcode_name($from); + my $jto = &get_jcode_name($to); + return $to if ( uc($jfrom) eq uc($jto) ); + if ( $jfrom && $jto ) { + &load_jcode(); + if ( defined $Jcode::VERSION ) { + Jcode::convert( $txtref, $jto, $jfrom ); + } + else { + return $self->die( "Jcode.pm is required: $from to $to" ); + } + } + else { + return $self->die( "Encode.pm is required: $from to $to" ); + } + } + $to; +} + +sub load_jcode { + return if defined $Jcode::VERSION; + local $@; + eval { require Jcode; }; +} + +sub load_encode { + return if defined $Encode::VERSION; + local $@; + eval { require Encode; }; +} + +sub latin1_to_utf8 { + my $strref = shift; + $$strref =~ s{ + ([\x80-\xFF]) + }{ + pack( 'C2' => 0xC0|(ord($1)>>6),0x80|(ord($1)&0x3F) ) + }exg; +} + +sub get_jcode_name { + my $src = shift; + my $dst; + if ( $src =~ /^utf-?8$/i ) { + $dst = 'utf8'; + } + elsif ( $src =~ /^euc.*jp(-?(win|ms))?$/i ) { + $dst = 'euc'; + } + elsif ( $src =~ /^(shift.*jis|cp932|windows-31j)$/i ) { + $dst = 'sjis'; + } + elsif ( $src =~ /^iso-2022-jp/ ) { + $dst = 'jis'; + } + $dst; +} + +sub xml_escape { + my $str = shift; + return '' unless defined $str; + # except for TAB(\x09),CR(\x0D),LF(\x0A) + $str =~ s{ + ([\x00-\x08\x0B\x0C\x0E-\x1F\x7F]) + }{ + sprintf( '&#%d;', ord($1) ); + }gex; + $str =~ s/&(?!#(\d+;|x[\dA-Fa-f]+;))/&/g; + $str =~ s//>/g; + $str =~ s/'/'/g; + $str =~ s/"/"/g; + $str; +} + +sub xml_unescape { + my $str = shift; + my $map = {qw( quot " lt < gt > apos ' amp & )}; + $str =~ s{ + (&(?:\#(\d+)|\#x([0-9a-fA-F]+)|(quot|lt|gt|apos|amp));) + }{ + $4 ? $map->{$4} : &char_deref($1,$2,$3); + }gex; + $str; +} + +sub char_deref { + my( $str, $dec, $hex ) = @_; + if ( defined $dec ) { + return &code_to_utf8( $dec ) if ( $dec < 256 ); + } + elsif ( defined $hex ) { + my $num = hex($hex); + return &code_to_utf8( $num ) if ( $num < 256 ); + } + return $str; +} + +sub code_to_utf8 { + my $code = shift; + if ( $code < 128 ) { + return pack( C => $code ); + } + elsif ( $code < 256 ) { + return pack( C2 => 0xC0|($code>>6), 0x80|($code&0x3F)); + } + elsif ( $code < 65536 ) { + return pack( C3 => 0xC0|($code>>12), 0x80|(($code>>6)&0x3F), 0x80|($code&0x3F)); + } + return shift if scalar @_; # default value + sprintf( '&#x%04X;', $code ); +} + +1; diff --git a/share/perl/test/OpenSimTest.pm b/share/perl/test/OpenSimTest.pm new file mode 100644 index 0000000..a24ae22 --- /dev/null +++ b/share/perl/test/OpenSimTest.pm @@ -0,0 +1,53 @@ +package OpenSimTest; + +use strict; +use PerformanceTest; +use OpenSimTest::Config; +use OpenSimTest::UserTester; +use OpenSimTest::GridTester; +use OpenSimTest::AssetTester; +use OpenSimTest::InventoryTester; + +sub init { + UserTester::init(); + GridTester::init(); + AssetTester::init(); + InventoryTester::init(); +} + +sub SingleTest { + my $url = shift; + my $methodname = shift; + my @ARGS = @_; + + if (!$OpenSimTest::Config::HANDLER_LIST{$methodname}) { + Carp::croak("unknown handler name: [$methodname]"); + } else { + my $handler = $OpenSimTest::Config::HANDLER_LIST{$methodname}; + my $result = $handler->($url, @ARGS); + return $result; + } +} + +sub PerformanceCompare { + my $server_name = shift; + my $count = shift; + my @args = @_; + my $test = new PerformanceTest(); + { + my @params = @args; + unshift(@params, $OpenSimTest::Config::APACHE_SERVERS{$server_name}); + $test->add_test("APACHE::$args[0]", \&OpenSimTest::SingleTest, \@params); + } + { + my @params = @args; + unshift(@params, $OpenSimTest::Config::OPENSIM_SERVERS{$server_name}); + $test->add_test("OPENSIM::$args[0]", \&OpenSimTest::SingleTest, \@params); + } + $test->set_count($count); + $test->start(); + print "\n\n"; + #$test->bref_result(); +} + +1; diff --git a/share/perl/test/OpenSimTest/AssetTester.pm b/share/perl/test/OpenSimTest/AssetTester.pm new file mode 100644 index 0000000..ba05205 --- /dev/null +++ b/share/perl/test/OpenSimTest/AssetTester.pm @@ -0,0 +1,17 @@ +package AssetTester; + +use strict; +use XML::Serializer; +use OpenSim::Utility; + +sub init { + &OpenSimTest::Config::registerHandler("get_asset", \&_get_asset); +} + +sub _get_asset { + my $url = shift || $OpenSimTest::Config::ASSET_SERVER_URL; + my $asset_id = shift; + my $res = &OpenSim::Utility::HttpGetRequest($url . "/assets/" . $asset_id) . "\n"; +} + +1; diff --git a/share/perl/test/OpenSimTest/Config.pm b/share/perl/test/OpenSimTest/Config.pm new file mode 100644 index 0000000..14ab3ed --- /dev/null +++ b/share/perl/test/OpenSimTest/Config.pm @@ -0,0 +1,53 @@ +package OpenSimTest::Config; + +use strict; + +my $apache_server_host = "localhost"; +my $opensim_server_host = "localhost"; + +# REGION +our $SIM_RECV_KEY = ""; +our $SIM_SEND_KEY = ""; +# ASSET +#our $ASSET_SERVER_URL = "http://127.0.0.1:8003/"; +our $ASSET_SERVER_URL = "http://$apache_server_host/opensim/asset.cgi"; +our $ASSET_RECV_KEY = ""; +our $ASSET_SEND_KEY = ""; +# USER +#our $USER_SERVER_URL = "http://127.0.0.1:8001/"; +our $USER_SERVER_URL = "http://$apache_server_host/opensim/user.cgi"; +our $USER_RECV_KEY = ""; +our $USER_SEND_KEY = ""; +# GRID +#our $GRID_SERVER_URL = "http://127.0.0.1:8001/"; +our $GRID_SERVER_URL = "http://$apache_server_host/opensim/grid.cgi"; +our $GRID_RECV_KEY = ""; +our $GRID_SEND_KEY = ""; +# INVENTORY +#our $INVENTORY_SERVER_URL = "http://127.0.0.1:8004"; +our $INVENTORY_SERVER_URL = "http://$apache_server_host/opensim/inventory.cgi"; +# handler list +our %HANDLER_LIST = (); + +our %APACHE_SERVERS = ( + user => "http://$apache_server_host/opensim/user.cgi", + grid => "http://$apache_server_host/opensim/grid.cgi", + asset => "http://$apache_server_host/opensim/asset.cgi", + inventory => "http://$apache_server_host/opensim/inventory.cgi", +); + +our %OPENSIM_SERVERS = ( + user => "http://$opensim_server_host:8002", + grid => "http://$opensim_server_host:8001", + asset => "http://$opensim_server_host:8003", + inventory => "http://$opensim_server_host:8004", +); + +sub registerHandler { + my ($name, $func) = @_; + $HANDLER_LIST{$name} = $func; +} + + +1; + diff --git a/share/perl/test/OpenSimTest/GridTester.pm b/share/perl/test/OpenSimTest/GridTester.pm new file mode 100644 index 0000000..61fef6b --- /dev/null +++ b/share/perl/test/OpenSimTest/GridTester.pm @@ -0,0 +1,62 @@ +package GridTester; + +use strict; +use OpenSim::Utility; + +sub init { + &OpenSimTest::Config::registerHandler("simulator_login", \&_simulator_login); + &OpenSimTest::Config::registerHandler("simulator_data_request", \&_simulator_data_request); + &OpenSimTest::Config::registerHandler("simulator_after_region_moved", \&_simulator_after_region_moved); + &OpenSimTest::Config::registerHandler("map_block", \&_map_block); +} + +sub _simulator_login { + my $url = shift || $OpenSimTest::Config::GRID_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + "authkey" => "null", + "UUID" => $param[0], + "sim_ip" => $param[1], + "sim_port" => $param[2], + "region_locx" => 1000, + "region_locy" => 1000, + "sim_name" => "OpenTest", + "http_port" => 9000, + "remoting_port" => 8895, + "map-image-id" => "0e5a5e87-08d9-4b37-9b8e-a4c3c4e409ab", + ); + return &OpenSim::Utility::XMLRPCCall($url, "simulator_login", \%xml_rpc_param); +} + +sub _map_block { + my $url = shift || $OpenSimTest::Config::GRID_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + xmin => $param[0], + ymin => $param[1], + xmax => $param[2], + ymax => $param[3], + ); + return &OpenSim::Utility::XMLRPCCall($url, "map_block", \%xml_rpc_param); +} + +sub _simulator_data_request { + my $url = shift || $OpenSimTest::Config::GRID_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + region_handle => $param[0], + authkey => undef, + ); + return &OpenSim::Utility::XMLRPCCall($url, "simulator_data_request", \%xml_rpc_param); +} + +sub _simulator_after_region_moved { + my $url = shift || $OpenSimTest::Config::GRID_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + UUID => $param[0], + ); + return &OpenSim::Utility::XMLRPCCall($url, "simulator_after_region_moved", \%xml_rpc_param); +} + +1; diff --git a/share/perl/test/OpenSimTest/InventoryTester.pm b/share/perl/test/OpenSimTest/InventoryTester.pm new file mode 100644 index 0000000..76615b1 --- /dev/null +++ b/share/perl/test/OpenSimTest/InventoryTester.pm @@ -0,0 +1,116 @@ +package InventoryTester; + +use strict; +use XML::Serializer; +use OpenSim::Utility; + +sub init { + &OpenSimTest::Config::registerHandler("create_inventory", \&_create_inventory); + &OpenSimTest::Config::registerHandler("root_folders", \&_root_folders); + &OpenSimTest::Config::registerHandler("get_inventory", \&_get_inventory); + &OpenSimTest::Config::registerHandler("new_item", \&_new_item); + &OpenSimTest::Config::registerHandler("new_folder", \&_new_folder); +} + +sub _apache_flag { + my $url = shift; + return $url =~ /inventory.cgi/ ? 1 : 0; +} + +sub _new_folder { + my $url = shift || $OpenSimTest::Config::INVENTORY_SERVER_URL; + my $post_data =<<"POSTDATA"; + +New Folder + +b9cb58e8-f3c9-4af5-be47-029762baa68f + + +500ea141-2967-49e2-9e18-fcdedffe68df + + +aa6f9220-c945-0b23-6141-43c9ef734100 + +-1 +0 + +POSTDATA + if (&_apache_flag($url)) { + $post_data = "POSTDATA=" . $post_data; # TODO: bad temporary solution + } + my $res = &OpenSim::Utility::HttpPostRequest($url . "/NewFolder/", $post_data) . "\n"; +} + +sub _new_item { + my $url = shift || $OpenSimTest::Config::INVENTORY_SERVER_URL; + my $post_data =<<"POSTDATA"; + + +f975d038-3bd7-4e8b-a945-f46b0c962ee3 + + +5f50f162-1cc6-4907-99be-a4c81d7f5e10 + +6 +6 + +7018dc23-43a9-493f-b3f7-869a6bbad0f3 + + +b9cb58e8-f3c9-4af5-be47-029762baa68f + + +b9cb58e8-f3c9-4af5-be47-029762baa68f + +Primitive + +2147483647 +526053692 +2147483647 +0 + +POSTDATA + if (&_apache_flag($url)) { + $post_data = "POSTDATA=" . $post_data; + } + my $res = &OpenSim::Utility::HttpPostRequest($url . "/NewItem/", $post_data) . "\n"; +} + +sub _get_inventory { + my $url = shift || $OpenSimTest::Config::INVENTORY_SERVER_URL; + my $uuid = shift; + my $serializer = new XML::Serializer($uuid, "guid"); + my $post_data = $serializer->to_string(XML::Serializer::WITH_HEADER); + if (&_apache_flag($url)) { + $post_data = "POSTDATA=" . $post_data; + } + my $res = &OpenSim::Utility::HttpPostRequest($url . "/GetInventory/", $post_data) . "\n"; + return 1; +} + +sub _create_inventory { + my $url = shift || $OpenSimTest::Config::INVENTORY_SERVER_URL; + my $uuid = shift; + my $serializer = new XML::Serializer($uuid, "guid"); + my $post_data = $serializer->to_string(XML::Serializer::WITH_HEADER); + if (&_apache_flag($url)) { + $post_data = "POSTDATA=" . $post_data; + } + my $res = &OpenSim::Utility::HttpPostRequest($url . "/CreateInventory/", $post_data) . "\n"; + return 1; +} + +sub _root_folders { + my $url = shift || $OpenSimTest::Config::INVENTORY_SERVER_URL; + my $uuid = shift; + my $serializer = new XML::Serializer($uuid, "guid"); + my $post_data = $serializer->to_string(XML::Serializer::WITH_HEADER); + if (&_apache_flag($url)) { + $post_data = "POSTDATA=" . $post_data; + } + my $res = &OpenSim::Utility::HttpPostRequest($url . "/RootFolders/", $post_data) . "\n"; + return 1; +} + +1; + diff --git a/share/perl/test/OpenSimTest/UserTester.pm b/share/perl/test/OpenSimTest/UserTester.pm new file mode 100644 index 0000000..194102a --- /dev/null +++ b/share/perl/test/OpenSimTest/UserTester.pm @@ -0,0 +1,53 @@ +package UserTester; + +use strict; +use Digest::MD5; +use OpenSim::Utility; + +my $user_server_url; + +sub init { + &OpenSimTest::Config::registerHandler("login_to_simulator", \&_login_to_simulator); + &OpenSimTest::Config::registerHandler("get_user_by_name", \&_get_user_by_name); + &OpenSimTest::Config::registerHandler("get_user_by_uuid", \&_get_user_by_uuid); + &OpenSimTest::Config::registerHandler("get_avatar_picker_avatar", \&_get_avatar_picker_avatar); +} + +sub _login_to_simulator { + my $url = shift || $OpenSimTest::Config::USER_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + first => $param[0], + last => $param[1], + passwd => "\$1\$" . Digest::MD5::md5_hex($param[2]), + start => "last", + version => "1.18.3.5", + mac => "cc82e1e2bfd24e5424d66b4fd3f70d55", + ); + return &OpenSim::Utility::XMLRPCCall($url, "login_to_simulator", \%xml_rpc_param); +} + +sub _get_user_by_name { + my $url = shift || $OpenSimTest::Config::USER_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + avatar_name => $param[0], + ); + return &OpenSim::Utility::XMLRPCCall($url, "get_user_by_name", \%xml_rpc_param); +} + +# sample uuid: +# db836502-de98-49c9-9edc-b90a67beb0a8 +sub _get_user_by_uuid { + my $url = shift || $OpenSimTest::Config::USER_SERVER_URL; + my @param = @_; + my %xml_rpc_param = ( + avatar_uuid => $param[0], + ); + return &OpenSim::Utility::XMLRPCCall($url, "get_user_by_uuid", \%xml_rpc_param); +} + +sub _get_avatar_picker_avatar { +} + +1; diff --git a/share/perl/test/PerformanceTest.pl b/share/perl/test/PerformanceTest.pl new file mode 100644 index 0000000..5f570e8 --- /dev/null +++ b/share/perl/test/PerformanceTest.pl @@ -0,0 +1,78 @@ +#!/usr/bin/perl + +# Usage: +# ./PerformanceTest.pl +# 2 variables should be changed: +# Line 14: $fork_limit +# Line 13: $benchmark_loop_count +# + +use strict; +use OpenSimTest; + +my $script = "./PerformanceTest.pl"; +my $fork_limit = 50; # the number of process +my $benchmark_loop_count = 10000; # the number of requests sent by each process + +my @child_pid = (); + +for(1..$fork_limit) { + my $pid = fork; + if ($pid) { + &parent_do($pid); + } elsif ($pid == 0) { + &child_do; + exit(0); + } else { + die "could not fork"; + } +} + +foreach (@child_pid) { + waitpid($_, 0); +} + + +sub parent_do { + my $pid = shift; + push(@child_pid, $pid); +} + +sub child_do { + #for(1..10000) { + # print "$_ "; + #} + &OpenSimTest::init(); + # user + &OpenSimTest::PerformanceCompare("user", $benchmark_loop_count, "get_user_by_name", "Test User"); + &OpenSimTest::PerformanceCompare("user", $benchmark_loop_count, "get_user_by_uuid", "db836502-de98-49c9-9edc-b90a67beb0a8"); + # grid + &OpenSimTest::PerformanceCompare("grid", $benchmark_loop_count, "simulator_login", "3507f395-88e5-468c-a45f-d4fd96a1c668", "10.8.1.148", 9000); + &OpenSimTest::PerformanceCompare("grid", $benchmark_loop_count, "simulator_data_request", "1099511628032000"); + &OpenSimTest::PerformanceCompare("grid", $benchmark_loop_count, "map_block", 999, 999, 1001, 1001); + # asset + &OpenSimTest::PerformanceCompare("asset", $benchmark_loop_count, "get_asset", "00000000000022223333000000000001"); + # inventory + &OpenSimTest::PerformanceCompare("inventory", $benchmark_loop_count, "root_folders", "b9cb58e8-f3c9-4af5-be47-029762baa68f"); + &OpenSimTest::PerformanceCompare("inventory", $benchmark_loop_count, "get_inventory", "b9cb58e8-f3c9-4af5-be47-029762baa68f"); +} + +__END__ +my $count = 10000; + +&OpenSimTest::init(); +# user +#&OpenSimTest::PerformanceCompare("user", $count, "get_user_by_name", "Test User"); +#&OpenSimTest::PerformanceCompare("user", $count, "get_user_by_uuid", "db836502-de98-49c9-9edc-b90a67beb0a8"); +# grid +#&OpenSimTest::PerformanceCompare("grid", $count, "simulator_login", "3507f395-88e5-468c-a45f-d4fd96a1c668", "10.8.1.148", 9000); +#&OpenSimTest::PerformanceCompare("grid", $count, "simulator_data_request", "1099511628032000"); +#&OpenSimTest::PerformanceCompare("grid", $count, "map_block", 999, 999, 1001, 1001); +# asset +&OpenSimTest::PerformanceCompare("asset", $count, "get_asset", "00000000000022223333000000000001"); +# inventory +#&OpenSimTest::PerformanceCompare("inventory", $count, "create_inventory", "b9cb58e8-f3c9-4af5-be47-029762baa68f"); +#&OpenSimTest::PerformanceCompare("inventory", $count, "root_folders", "b9cb58e8-f3c9-4af5-be47-029762baa68f"); +#&OpenSimTest::PerformanceCompare("inventory", $count, "get_inventory", "b9cb58e8-f3c9-4af5-be47-029762baa68f"); +#&OpenSimTest::PerformanceCompare("inventory", $count, "new_item"); +#&OpenSimTest::PerformanceCompare("inventory", $count, "new_folder"); diff --git a/share/perl/test/SingleTest.pl b/share/perl/test/SingleTest.pl new file mode 100644 index 0000000..e6be081 --- /dev/null +++ b/share/perl/test/SingleTest.pl @@ -0,0 +1,21 @@ +#!/usr/bin/perl + +# usage: +# ./SingleTest.pl $URL $METHOD @PARAMETERS +# example +# ./SingleTest.pl http://127.0.0.1/user.cgi get_user_by_name "Test User" +# ./SingleTest.pl http://127.0.0.1/grid.cgi simulator_login 3507f395-88e5-468c-a45f-d4fd96a1c668 10.8.1.148 9000 +# ./SingleTest.pl http://127.0.0.1/grid.cgi map_block 999 999 1001 1001 +# ./SingleTest.pl http://127.0.0.1/asset.cgi get_asset 00000000000022223333000000000001 +# + +use strict; +use Data::Dump; +use OpenSimTest; + +&OpenSimTest::init(); +my $url = shift @ARGV; +#my $url = "http://localhost:8002"; +my $result = &OpenSimTest::SingleTest($url, @ARGV); +Data::Dump::dump($result); + diff --git a/share/perl/user.cgi b/share/perl/user.cgi new file mode 100644 index 0000000..3fa63aa --- /dev/null +++ b/share/perl/user.cgi @@ -0,0 +1,28 @@ +#!/usr/bin/perl -w + +use strict; +use Carp; +use XML::RPC; +use MyCGI; +use OpenSim::Utility; +use OpenSim::UserServer; + +my $param = &MyCGI::getParam(); +my $request = $param->{'POSTDATA'}; +&OpenSim::Utility::Log("user", "request", $request); +my $xmlrpc = new XML::RPC(); +my $response = $xmlrpc->receive($request, \&XMLRPCHandler); +&OpenSim::Utility::Log("user", "response", $response); +&MyCGI::outputXml("utf-8", $response); + +sub XMLRPCHandler { + my ($methodname, @param) = @_; + my $handler_list = &OpenSim::UserServer::getHandlerList(); + if (!$handler_list->{$methodname}) { + Carp::croak("?"); + } else { + my $handler = $handler_list->{$methodname}; + $handler->(@param); + } +} + -- cgit v1.1