Imported Upstream version 2.18 upstream/2.18
authorAnas Nashif <anas.nashif@intel.com>
Fri, 28 Dec 2012 03:04:28 +0000 (19:04 -0800)
committerAnas Nashif <anas.nashif@intel.com>
Fri, 28 Dec 2012 03:04:28 +0000 (19:04 -0800)
26 files changed:
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
META.yml [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
lib/XML/Simple.pm [new file with mode: 0644]
lib/XML/Simple/FAQ.pod [new file with mode: 0644]
maketest [new file with mode: 0644]
t/0_Config.t [new file with mode: 0644]
t/1_XMLin.t [new file with mode: 0644]
t/1_XMLin.xml [new file with mode: 0644]
t/2_XMLout.t [new file with mode: 0644]
t/3_Storable.t [new file with mode: 0644]
t/4_MemShare.t [new file with mode: 0644]
t/5_MemCopy.t [new file with mode: 0644]
t/6_ObjIntf.t [new file with mode: 0644]
t/7_SaxStuff.t [new file with mode: 0644]
t/8_Namespaces.t [new file with mode: 0644]
t/9_Strict.t [new file with mode: 0644]
t/A_XMLParser.t [new file with mode: 0644]
t/B_Hooks.t [new file with mode: 0644]
t/desertnet.src [new file with mode: 0644]
t/lib/TagsToUpper.pm [new file with mode: 0755]
t/srt.xml [new file with mode: 0644]
t/subdir/test2.xml [new file with mode: 0644]
t/test1.xml [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..2632e51
--- /dev/null
+++ b/Changes
@@ -0,0 +1,266 @@
+Revision history for Perl module XML::Simple
+
+2.18  Aug 15 2007
+      - Non-unique key attribute values now trigger a warning (or a fatal
+        error in strict mode) rather than silently discarding data (patch
+        from Daniel Baysinger)
+
+2.17  Aug 02 2007
+      - Added parse_string(), parse_file() and parse_fh() methods
+      - Added default_config_file(), and build_simple_tree() hook methods
+      - Tweak to implementation of exporting (patch from Stuart Moore)
+      - Documented hook methods
+      - Fixed test suite race condition (RT#28603 from Andreas J. König)
+
+2.16  Oct 30 2006
+      - Added test/fix for bad GroupTags option (report from Lee Goddard)
+      - Added new_hashref() hook method
+      - refactored cache save/restore methods for easier overriding
+
+2.15  Oct 03 2006
+      - Makefile.PL changes: reject known-bad PurePerl and RTF parser modules;
+        default to XML::SAX::Expat if no parser installed
+      - allow '.' characters in variable names (suggested by Cosimo Streppone)
+      - fix output of undefs in arrayrefs with SuppressEmpty (reported by
+        かんな - Kanna)
+      - tidy up code and docs around lexical filehandle passed to OutputFile
+        (report from Helge Sauer)
+      - reduce memory usage by passing XML strings by reference (patch from
+        Dan Sully)
+
+2.14  Jan 29 2005
+      - unlink and lock fixes for VMS (patch from Peter (Stig) Edwards)
+
+2.13  Nov 17 2004
+      - Fixed bug where NoIndent broke KeyAttr (reported by David Haas)
+      - Added copy_hash helper method which may be overridden to avoid
+        tied hashes becoming untied during XMLout (patch from Jan Sundberg)
+      - Fixed bug where GroupTags corrupted source hashref in XMLout
+        (reported by Bram)
+      - Tweaks to SuppressEmpty for undef with XMLout (report from jamesb),
+        behaviour now matches docs and additional behaviour of setting
+        option to 1 will skip undefined values altogether
+
+2.12  Apr 05 2004
+      - added NumericEscape option
+      - added ValueAttr option (patch from Anton Berezin)
+      - suppress 'wide character in print' warning (reported by Dawei Lin)
+
+2.11  Mar 02 2004
+      - Fixed hash ordering assumption in a new test (reported by Jost Krieger)
+
+2.10  Feb 29 2004
+      - Added AttrIndent option (patch from Volker Moell)
+      - Hash keys are now sorted alphabetically by default; enable the
+        new NoSort option if you don't want this (patch from Volker Moell)
+      - Fixed bug where disabling array folding broke anonymous array handling
+      - Fixed bug when unfolding a tied hash
+      - SuppressEmpty patch from Douglas Wilson
+      - Numerous test improvements - Devel::Cover rocks!
+      - POD update re XMLin(XMLout($data)) caveats (bug report from Slaven
+        Rezic)
+
+2.09  Sep 09 2003
+      - Makefile.PL makeover contributed by Joshua Keroes
+      - fixed hash ordering assumption in test script (reported by Michel
+        Rodriguez)
+      - POD updates
+      - updated link to Perl XML FAQ
+
+2.08  Jun 13 2003
+      - fixed variable expansion not happening in attributes (patch from Paul
+        Bussé)
+
+2.07  May 20 2003
+      - added test to catch old versions of Storable which lack locking support
+      - removed new-style loop which broke on 5.005_03
+      - suppress more uninitialised variable warnings
+
+2.06  May 18 2003
+      - fixed strict mode requiring ForceArray on output (fix from Igor Román
+        Mariño)
+      - fixed warnings about uninitialised values
+      - minor POD update (link to FAQ)
+
+2.05  Apr 16 2003
+      - fixed warnings when NormaliseSpace undefined (reported by Peter
+        Scott and others)
+      - added support for specifying ForceArray using regular expressions
+        (patch from Jim Cromie)
+      - added check to escape_value to guard against undefined argument
+        (reported by Henrik Gemal)
+      - added NoIndent option (requested by Afroze Husain Zubairi)
+
+2.04  Apr 10 2003
+      - integrated a patch from Michel Rodriguez
+        + new facility for removing extra levels of indirection (using
+          the new 'GroupTags' option)
+        + new facility for rolling the dreaded 'content' hash up into a
+          scalar if there are no keys left after array folding (using the
+          '-' prefix mode on the ContentKey option)
+        + new facility for doing variable substitution in the XML; variables
+          can be defined in Perl (using the new 'Variables' option) or in 
+          the XML document (using the new 'VarAttr' option)
+      - added 'NormaliseSpace' option for tidying up hash keys and other
+        text content if required (feature requested by Alex Manoussakis)
+      - option names are now case-insensitive and can include underscores
+      - XMLin() and XMLout() are now aliased to xml_in() and xml_out() when
+        called as methods or imported explicitly
+      - option names passed to XML::Simple->new() are now validated
+
+2.03  Jan 20 2003
+      - fixed circular reference check which was incorrectly catching
+        'parallel' references (patch from Theo Lengyel)
+
+2.02  Dec 15 2002
+      - changed Storable calls to use locking (reported by Randal Schwarz)
+
+2.01  Dec 11 2002
+      - fixed bug whereby :strict mode required forcearray on 
+        XMLout() (reported by Ville Skytta)
+
+2.00  Dec 08 2002
+      - first production release with SAX support
+      - added support for 'strict mode' using :strict import tag
+      - removed locking code (as it was incompatible with iThreads)
+      - integrated patch for test failures from Sean Campbell
+      - fixed stringification of references during folding (reported
+        by Trond Michelsen)
+      - fixed incompatability with Tie::IxHash (reported by
+        Venkataramana Mokkapati)
+      - POD: alphabetised options (patch from John Borwick)
+      - POD: updated suppressempty (patch from Kjetil Kjernsmo)
+      - added FAQ.pod to distribution and added new questions
+
+1.08_01 Feb 14 2002 - beta release for testing SAX support
+      - fixed errors with default namespace handling
+      - minor POD updates
+
+1.08  Feb 09 2002
+      - re-release of 1.06 (stable) with minor updates ...
+      - searchpath option now defaults to current directory if not set
+      - fix to Storable test routine for test failures on Win32
+      - removed obselete 'convert' script from distribution
+
+1.07b Feb 05 2002 - beta release for testing SAX support
+      - added SAX support including:
+        + using SAX parsers
+        + acting as a SAX handler 
+        + generating SAX events from XMLout() with new Handler option
+        + acting as a SAX filter (via new DataHandler option)
+      - added $ENV{XML_SIMPLE_PREFERRED_PARSER} and
+        $XML::Simple::PREFERRED_PARSER for selecting a parser module
+      - added namespace support (SAX only) with nsexpand option for both 
+        XMLin() and XMLout()
+      - searchpath now defaults to current directory
+      - parseropts option now officially deprecated
+      - removed obselete 'convert' script from distribution
+      - many POD updates (more to come)
+
+1.06  Nov 19 2001
+      - fixed version number in default xmldecl (thanks to Matt Sergeant for
+        bug report and patch)
+      - updated contact email address for author
+
+
+1.05  Aug 31 2000
+      - code re-org to make internals all OO for easier extending
+      - added 'noattr' option to tell XMLout() not to use attributes (only
+        nested elements) and XMLin() to discard attributes
+      - added 'suppressempty' option to tell XMLin what to do with elements
+        with no attributes and no content
+      - added 'parseropts' option for specifying options which should be
+        passed to the underlying XML::Parser object
+      - added 'forcecontent' option to force text content to parse to a
+        hash value even if the element has no attributes
+      - fix for forcearray getting applied to text content
+      - integrated patch from Paul Lindner to work around filenames sometimes
+        being seen as XML when running under mod_perl
+      - integrated patch from Edward Avis: filename '-' means stdin
+      - fixed bug where a missing key attribute could cause a crash
+      - added a warning message for above situation
+      - added 'support' for CDATA sections - they always worked, but now
+        they're in the test suite which should ensure they keep working
+      - fixed error message when caching enabled but parsing from filehandle
+      - fixed empty elements being skipped by XMLout() when folding enabled
+      - fixed text content of '0' being skipped by XMLout()
+
+1.04  Apr 03 2000
+      - fix for text content being skipped by XMLout
+      - added (optional) OO interface for changing default options
+      - added 'keeproot' option (requested by Mark D. Anderson - MDA)
+      - added 'contentkey' option (also requested by MDA)
+      - incorporated 'forcearray' as arrayref patch from Andrew McNaughton
+
+1.03  Mar 05 2000
+      - added 'maketest' script for make impaired platforms
+      - yet more cross platform robustness added to test scripts incl
+        workaround for Win32 problem where writing to file changed contents
+        but not timestamp(!)
+      - backed out one overzealous use of File::Spec in test script
+      - POD updates including XML::Twig description contributed by Michel
+        Rodriguez
+
+1.02b Feb 16 2000 - limited distribution beta
+      - reinstated locking with new backwards compatibility code
+      - fixed platform dependant pathname handling to use File::Basename &
+        File::Spec in XML::Simple.pm and test scripts
+      - fixed bug causing XMLout() to incorrectly barf on what it thought was
+        a recursive data structure
+      - removed spurious checking code which stopped XMLout unfolding a
+        single nested hash
+      - fixed t/4_MemShare.t to gracefully cope with the absense of utime()
+      - changed t/3_Storable.t and t/5_MemCopy.t to skip gracefully if no
+        Storable.pm
+      - removed superflous eval blocks around requires
+
+1.01  Dec  1 1999
+      - removed faulty locking code pending a fix
+
+1.00  Nov 25 1999
+      - added escaping feature + noescape option
+      - added xmldecl option
+      - further tidy ups for thread safing
+      - more POD revisions (incl: pointers to other modules)
+
+0.95  Nov  2 1999
+      - added rootname option
+      - added outputfile option
+      - lots of internal tidy ups for thread safing
+      - fixed bug in check for XML string to XMLin()
+      - extra tests (esp option handling)
+
+0.90  Oct 14 1999 (first beta release)
+      - module renamed to XML::Simple ready for CPAN upload
+      - XMLToOpt() renamed to XMLin()
+      - OptToXML() renamed to XMLout()
+      - added 'convert' script
+
+0.05  Sep 18 1999
+      - fixed location of XML.pm in distribution (make install didn't work)
+      - added tests for MemCopy
+      - fixed ABSTRACT_FROM in Makefile.PL
+      - fixed PREREQ_PM in Makefile.PL
+
+0.04  Aug 10 1999
+      - added caching using Storable.pm
+      - updated MANIFEST to include missing test files
+
+0.03  Jun 20 1999
+      - rewrite of OptToXML
+      - anonymous array support
+      - more and better test routines
+      - POD updates
+
+0.02  Jun 10 1999
+      - added support for OptToXML
+      - fixed searchpath inconsistencies
+      - added 'forcearray' option
+      - POD improvements
+      - much improved test routines
+
+0.01  May 27 1999
+      - original version; created by h2xs 1.18
+      - module called 'Getopt::XML'
+      - included basic XMLToOpt routine
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..b46b0db
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,26 @@
+Changes
+lib/XML/Simple.pm
+lib/XML/Simple/FAQ.pod
+Makefile.PL
+maketest
+MANIFEST
+META.yml                       Module meta-data (added by MakeMaker)
+README
+t/0_Config.t
+t/1_XMLin.t
+t/1_XMLin.xml
+t/2_XMLout.t
+t/3_Storable.t
+t/4_MemShare.t
+t/5_MemCopy.t
+t/6_ObjIntf.t
+t/7_SaxStuff.t
+t/8_Namespaces.t
+t/9_Strict.t
+t/A_XMLParser.t
+t/B_Hooks.t
+t/desertnet.src
+t/lib/TagsToUpper.pm
+t/srt.xml
+t/subdir/test2.xml
+t/test1.xml
diff --git a/META.yml b/META.yml
new file mode 100644 (file)
index 0000000..cac2231
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,13 @@
+# http://module-build.sourceforge.net/META-spec.html
+#XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
+name:         XML-Simple
+version:      2.18
+version_from: lib/XML/Simple.pm
+installdirs:  site
+requires:
+    Test::Simple:                  0.41
+    XML::NamespaceSupport:         1.04
+    XML::SAX:                      0
+
+distribution_type: module
+generated_by: ExtUtils::MakeMaker version 6.17
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..9541df6
--- /dev/null
@@ -0,0 +1,94 @@
+# $Id: Makefile.PL,v 1.8 2006/10/03 01:02:20 grantm Exp $
+
+use ExtUtils::MakeMaker;
+
+BEGIN {
+  if($] < 5.006) {
+    warn
+      qq(This version of XML::Simple requires Perl version 5.6 or better.\n) .
+      qq(You might prefer to install an older version of XML::Simple\n\n) .
+      qq(  perl -MCPAN -e "install 'G/GR/GRANTM/XML-Simple-1.08.tar.gz'"\n\n);
+  }
+}
+
+require 5.006;
+
+my $make_params = {
+    'NAME'          => 'XML::Simple',
+    'VERSION_FROM'  => 'lib/XML/Simple.pm',
+    'DISTNAME'      => 'XML-Simple',
+    'PREREQ_PM'     => {
+                         Test::Simple          => 0.41,
+                       },
+    'dist'          => { COMPRESS => 'gzip --best', SUFFIX => 'gz' },
+    'AUTHOR'        => 'Grant McLean <grantm@cpan.org>',
+    'ABSTRACT_FROM' => 'lib/XML/Simple.pm',
+};
+
+print "Checking installed modules ...\n";
+
+if ( eval { require XML::SAX } && ! $@ ) {
+    my $default_parser = ref(XML::SAX::ParserFactory->parser());
+    if ($default_parser eq 'XML::SAX::PurePerl') {
+      my $version = XML::SAX->VERSION;
+      if($version > 0.12  and  $version < 0.15) {
+        die <<"EOF";
+=============================================================================
+
+  Fatal error: Your default XML parser (XML::SAX::PurePerl) is broken.
+
+  There are known bugs in the PurePerl parser included with version 0.13 
+  and 0.14 of XML::SAX.  The XML::Simple tests will fail with this parser.
+
+  One way to avoid the problem is to install XML::SAX::Expat - it will
+  install itself as the system default XML parser and then you will be able 
+  to install XML::Simple successfully.  XML::SAX::Expat is also much faster
+  than XML::SAX::PurePerl so you probably want it anyway.
+
+=============================================================================
+EOF
+      }
+    }
+    elsif ($default_parser eq 'XML::SAX::RTF') {
+      die <<"EOF";
+=============================================================================
+
+  Fatal error: Your default XML parser (XML::SAX::RTF) is broken.
+
+  The XML::SAX:RTF module is installed as the default XML parser on your
+  system.  This is a bug - although the module does generate SAX events,
+  it does not parse XML and should not register itself as an XML parser.
+
+  One way to avoid the problem is to install XML::SAX::Expat - it will
+  register itself as the system default XML parser and then you will be
+  able to install XML::Simple successfully.
+
+  Another solution would be to locate the XML/SAX/ParserDetails.ini file
+  and edit it to completely remove the section beginning [XML::SAX::RTF].
+
+=============================================================================
+EOF
+    }
+    print "XML::SAX is installed, it will be used by the test suite\n";
+    $make_params->{PREREQ_PM}->{'XML::SAX'} = 0;
+    $make_params->{PREREQ_PM}->{'XML::NamespaceSupport'} = 1.04;
+}
+elsif ( eval { require XML::Parser } && ! $@ ) {
+    print "XML::Parser is installed, it will be used by the test suite\n";
+    $make_params->{PREREQ_PM}->{'XML::Parser'} = 0;
+} 
+else {
+    print "You don't have either XML::SAX or XML::Parser installed!\n";
+    $make_params->{PREREQ_PM}->{'XML::SAX'} = 0;
+    $make_params->{PREREQ_PM}->{'XML::NamespaceSupport'} = 1.04;
+    # Hopefully the following line can be removed after next XML::SAX release
+    $make_params->{PREREQ_PM}->{'XML::SAX::Expat'} = 0;
+}
+
+eval { require Storable };
+if($@) {
+  print "Storable is not installed ... caching functions will not be available\n";
+}
+
+
+WriteMakefile(%$make_params);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..8a6289a
--- /dev/null
+++ b/README
@@ -0,0 +1,76 @@
+DESCRIPTION
+
+  XML::Simple - Easy API to read/write XML (esp config files)
+
+
+PREREQUISITES
+
+  XML::Simple requires a module capable of parsing XML - either XML::SAX or
+  XML::Parser must be installed (if you're running ActivePerl, you'll
+  already have XML::Parser installed).
+
+  If you have installed XML::SAX, it will be used by default.  You should
+  consider installing XML::SAX::Expat or XML::LibXML to replace the (slower)
+  PurePerl parser from the XML::SAX distribution.
+
+  If you install using the CPAN.pm shell and you do not have either XML::Parser
+  or XML::SAX installed, then XML::SAX will be installed by default.
+
+  To generate documents with namespaces, XML::NamespaceSupport is required.
+
+  The optional caching features of XML::Simple also require Storable.pm.
+
+
+WARNING MESSAGES FROM XML::SAX
+
+  When you use XML::Simple, you may see this warning message:
+
+    could not find ParserDetails.ini in C:/Perl/site/lib/XML/SAX
+
+  This means your XML::SAX installation is broken (perhaps you installed
+  version 0.10 from the ActiveState PPM repository).  Since it's broken 
+  anyway, the simplest way to suppress these warnings is to remove
+  C:/Perl/site/lib/XML/SAX.pm.
+
+  The procedure for correctly installing the latest version of XML::SAX
+  is documented here:
+
+    http://perl-xml.sourceforge.net/faq/#win32_cpan
+
+BUILDING/INSTALLING
+
+  Once the archive is unpacked, use these commands:
+
+      perl Makefile.PL
+      make
+      make test
+      make install
+
+  If for some reason, you can't run these commands, you can simple copy 
+  the Simple.pm file to your lib/XML directory (where Parser.pm lives).
+
+  If you want to test the module, but don't have 'make', try:
+
+      perl maketest
+
+
+STATUS
+
+  This version (2.16) is the current stable release.
+
+  Please send any feedback to the author: grantm@cpan.org
+
+  See 'Changes' for a detailed history.
+
+  See 'perldoc XML::Simple' for full documentation.
+
+  See 'perldoc XML::Simple::FAQ' for frequently asked questions.
+
+
+COPYRIGHT
+
+  Copyright 1999-2004 Grant McLean <grantm@cpan.org>
+
+  This library is free software; you can redistribute it
+  and/or modify it under the same terms as Perl itself.
+
diff --git a/lib/XML/Simple.pm b/lib/XML/Simple.pm
new file mode 100644 (file)
index 0000000..38c402d
--- /dev/null
@@ -0,0 +1,3284 @@
+# $Id: Simple.pm,v 1.40 2007/08/15 10:36:48 grantm Exp $
+
+package XML::Simple;
+
+=head1 NAME
+
+XML::Simple - Easy API to maintain XML (esp config files)
+
+=head1 SYNOPSIS
+
+    use XML::Simple;
+
+    my $ref = XMLin([<xml file or string>] [, <options>]);
+
+    my $xml = XMLout($hashref [, <options>]);
+
+Or the object oriented way:
+
+    require XML::Simple;
+
+    my $xs = XML::Simple->new(options);
+
+    my $ref = $xs->XMLin([<xml file or string>] [, <options>]);
+
+    my $xml = $xs->XMLout($hashref [, <options>]);
+
+(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(<?xml version='1.0' standalone='yes'?>);
+
+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 = <STDIN>;
+    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:
+#
+#  <element name>, [ { <attribute hashref> }, <element name>, [ ... ], ... ]
+#
+# 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 <element attrname="name">value</element> 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)),
+              '</', $name, ">", $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, '</', $name, ">", $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)),
+             '</', $name, ">$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, '</', $name, ">$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/&/&amp;/sg;
+  $data =~ s/</&lt;/sg;
+  $data =~ s/>/&gt;/sg;
+  $data =~ s/"/&quot;/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<foo> and a file of configuration options
+called B<foo.xml> containing this:
+
+  <config logdir="/var/log/foo/" debugfile="/tmp/foo.debug">
+    <server name="sahara" osname="solaris" osversion="2.6">
+      <address>10.0.0.101</address>
+      <address>10.0.1.101</address>
+    </server>
+    <server name="gobi" osname="irix" osversion="6.5">
+      <address>10.0.0.102</address>
+    </server>
+    <server name="kalahari" osname="linux" osversion="2.0.34">
+      <address>10.0.0.103</address>
+      <address>10.0.1.103</address>
+    </server>
+  </config>
+
+The following lines of code in B<foo>:
+
+  use XML::Simple;
+
+  my $config = XMLin();
+
+will 'slurp' the configuration options into the hashref $config (because no
+arguments are passed to C<XMLin()> 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<XMLout()>.
+
+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<XMLin()> and C<XMLout()>.  Note: you can explicity
+request the lower case versions of the function names: C<xml_in()> and
+C<xml_out()>.
+
+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<XML::Simple> 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<XMLin()> 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<XMLin()> 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<XMLin()> 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('<opt username="bob" password="flurp" />');
+
+=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<XMLin()>, it should
+return a data structure equivalent to the original (see caveats below). 
+
+The C<XMLout()> function can also be used to output the XML as SAX events
+see the C<Handler> 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<XMLout>.  (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<XMLout()>.  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<XML::Simple> 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<XML::Simple> does not support dumping binary data.
+
+If you break these rules, the current implementation of C<XMLout()> will 
+simply emit non-compliant XML which will be rejected if you try to read it
+back in.  (A later version of B<XML::Simple> 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<XMLout()> 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<XMLout()> and with C<XMLin()>.  If you still don't get the 
+expected results, you may prefer to use L<XML::Dumper> which is designed for
+exactly that purpose.
+
+Refer to L<"WHERE TO FROM HERE?"> if C<XMLout()> is too simple for your needs.
+
+
+=head1 OPTIONS
+
+B<XML::Simple> supports a number of options (in fact as each release of
+B<XML::Simple> 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<ForceArray> because you'll almost certainly want to turn it on
+
+=item *
+
+make sure you know what the C<KeyAttr> option does and what its default value is
+because it may surprise you otherwise (note in particular that 'KeyAttr'
+affects both C<XMLin> and C<XMLout>)
+
+=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<in>' if they are recognised by C<XMLin()> and
+'I<out>' if they are recognised by C<XMLout()>.
+
+=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<XMLout()>, 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<XML::Parser> module and parsing an XML file can consume a
+significant number of CPU cycles, it is often desirable to cache the output of
+C<XMLin()> for later reuse.
+
+When parsing from a named file, B<XML::Simple> 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<Storable.pm> 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<XML::Simple> 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<Storable.pm> 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('<opt one="1">Text</opt>', ContentKey => 'text')
+
+will parse to:
+
+  { 'one' => 1, 'text' => 'Text' }
+
+instead of:
+
+  { 'one' => 1, 'content' => 'Text' }
+
+C<XMLout()> 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<XMLin()> try a little harder to eliminate unnecessary 'content' keys after
+array folding.  For example:
+
+  XMLin(
+    '<opt><item name="one">First</item><item name="two">Second</item></opt>', 
+    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<XML::Simple> object as a SAX handler, it will return a
+'simple tree' data structure in the same format as C<XMLin()> 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<XML::Simple> 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:
+
+    <opt>
+      <name>value</name>
+    </opt>
+
+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<XMLin()> 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('<opt><x>text1</x><y a="2">text2</y></opt>', 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:
+
+  <opt>
+   <searchpath>
+     <dir>/usr/bin</dir>
+     <dir>/usr/local/bin</dir>
+     <dir>/usr/X11/bin</dir>
+   </searchpath>
+ </opt>
+
+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<< <searchpath> >> 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<KeyAttr>, the array
+folding will occur first and then the grouped element names will be eliminated.
+
+C<XMLout> 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<XMLout()> 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<XMLin()> 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('<config tempdir="/tmp" />', KeepRoot => 1)
+
+You'll be able to reference the tempdir as
+C<$config-E<gt>{config}-E<gt>{tempdir}> instead of the default
+C<$config-E<gt>{tempdir}>.
+
+Similarly, setting the 'KeepRoot' option to '1' will tell C<XMLout()> 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:
+
+    <opt>
+      <user login="grep" fullname="Gary R Epstein" />
+      <user login="stty" fullname="Simon T Tyson" />
+    </opt>
+
+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<XMLin()> will attempt to match attribute names in the order
+supplied.  C<XMLout()> 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<ForceArray> 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<XMLin()> 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:
+
+    <opt>
+      <user login="grep" fullname="Gary R Epstein" />
+      <user login="stty" fullname="Simon T Tyson" />
+    </opt>
+
+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<XMLout> will ignore hash keys starting with a '-'.
+
+=head2 NoAttr => 1 I<# in+out - handy>
+
+When used with C<XMLout()>, the generated XML will contain no attributes.
+All hash key/values will be represented as nested elements instead.
+
+When used with C<XMLin()>, any attributes in the XML will be ignored.
+
+=head2 NoEscape => 1 I<# out - seldom used>
+
+By default, C<XMLout()> will translate the characters 'E<lt>', 'E<gt>', '&' and
+'"' to '&lt;', '&gt;', '&amp;' and '&quot' 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<XMLout()>'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<XMLin()> 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<Note: You must be using a SAX parser for this option to work (ie: it does not
+work with XML::Parser)>.
+
+This option also controls whether C<XMLout()> 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<XMLout> will emit XML which is not well formed.
+
+I<Note: You must have the XML::NamespaceSupport module installed if you want
+C<XMLout()> 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: &#8364;) 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 => <file specifier> I<# out - handy>
+
+The default behaviour of C<XMLout()> 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<IO::Handle> - it simply assumes the object
+supports a C<print> method.
+
+=head2 ParserOpts => [ XML::Parser Options ] I<# in - don't use this>
+
+I<Note: This option is now officially deprecated.  If you find it useful, email
+the author with an example of what you use it for.  Do not use this option to
+set the ProtocolEncoding, that's just plain wrong - fix the XML>.
+
+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<XMLout()> 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<XMLin()>.
+Nevertheless, the option has been found to be useful in certain circumstances.
+
+=head2 SearchPath => [ list ] I<# in - handy>
+
+If you pass C<XMLin()> 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<XMLin()> but SearchPath is not defined, the
+file is assumed to be in the current directory.
+
+If the first parameter to C<XMLin()> 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<XMLin()> 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<XMLout()> 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:
+
+  <opt>
+    <colour value="red" />
+    <size   value="XXL" />
+  </opt>
+
+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<XMLout()> -
+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<XMLout()>.
+
+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<XMLout>).
+
+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<Variables>, 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<VarAttr>
+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<Variables> option.  For
+example:
+
+  XMLin( '<opt>
+            <dir name="prefix">/usr/local/apache</dir>
+            <dir name="exec_prefix">${prefix}</dir>
+            <dir name="bindir">${exec_prefix}/bin</dir>
+          </opt>',
+         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<XMLout()> to start with the optional XML
+declaration, simply set the option to '1'.  The default XML declaration is:
+
+        <?xml version='1.0' standalone='yes'?>
+
+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<XMLin()> or C<XMLout()>
+
+=item *
+
+to override methods in B<XML::Simple> 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<XML::Simple>'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<XMLin()> or C<XMLout()> 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<XMLin()> and C<XMLout()> routines may be
+called as C<xml_in()> or C<xml_out()>.  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<xml_in()> 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<xml_in()> 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<xml_in()> method but assumes the first argument is
+the name of a file containing XML.
+
+=item parse_fh(file_handle)
+
+Works exactly like the C<xml_in()> 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<XMLout()>
+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<XMLin()> (default: C<$0.xml>).
+
+=item build_simple_tree(filename, string)
+
+Called from C<XMLin()> or any of the parsing methods.  Takes either a file name
+as the first argument or C<undef> 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<XMLout()> 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<XMLout()>, takes a string and returns a copy of the string with
+XML character escaping rules applied.
+
+=item numeric_escape(string)
+
+Called from C<escape_value()>, to handle non-ASCII characters (depending on the
+value of the NumericEscape option).
+
+=item copy_hash(hashref, extra_key => value, ...)
+
+Called from C<XMLout()>, 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<MLDBM> module.  First, you would add a
+C<cache_read_dbm()> 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<cache_read_dbm()> 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<XML::Simple> 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<KeyAttr> option - if you can't be bothered
+reading about this option, turn it off with: KeyAttr => [ ]
+
+=item *
+
+Failing to explicitly set the C<ForceArray> 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 E<lt>partE<gt> 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 E<lt>partE<gt> 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<XML::Simple> 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<XML::Simple> 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<XMLout()>:
+
+  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<XML::Simple> 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<Note: In this last example, the 'Handler' option was specified in the call to
+C<XMLout()> but it could also have been specified in the constructor>.
+
+=head1 ENVIRONMENT
+
+If you don't care which parser module B<XML::Simple> uses then skip this
+section entirely (it looks more complicated than it really is).
+
+B<XML::Simple> will default to using a B<SAX> parser if one is available or
+B<XML::Parser> 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<XML::Simple> 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<XML::Parser> will be used (or C<XMLin()> will die if L<XML::Parser> 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<XML::SAX::ParserFactory.>
+If L<XML::SAX> is not installed, or the requested parser module is not
+installed, then C<XMLin()> will die.
+
+=item *
+
+If the 'preferred parser' is not defined at all (the normal default
+state), an attempt will be made to load L<XML::SAX>.  If L<XML::SAX> is
+installed, then a parser module will be selected according to
+L<XML::SAX::ParserFactory>'s normal rules (which typically means the last SAX
+parser installed).
+
+=item *
+
+if the 'preferred parser' is not defined and B<XML::SAX> is not
+installed, then B<XML::Parser> will be used.  C<XMLin()> will die if
+L<XML::Parser> is not installed.
+
+=back
+
+Note: The B<XML::SAX> distribution includes an XML parser written entirely in
+Perl.  It is very portable but it is not very fast.  You should consider
+installing L<XML::LibXML> or L<XML::SAX::Expat> 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<XML::Simple> 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<XMLin()> 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<eval> 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<XMLin()> reads the following very simple piece of XML:
+
+    <opt username="testuser" password="frodo"></opt>
+
+it returns the following data structure:
+
+    {
+      'username' => 'testuser',
+      'password' => 'frodo'
+    }
+
+The identical result could have been produced with this alternative XML:
+
+    <opt username="testuser" password="frodo" />
+
+Or this (although see 'ForceArray' option for variations):
+
+    <opt>
+      <username>testuser</username>
+      <password>frodo</password>
+    </opt>
+
+Repeated nested elements are represented as anonymous arrays:
+
+    <opt>
+      <person firstname="Joe" lastname="Smith">
+        <email>joe@smith.com</email>
+        <email>jsmith@yahoo.com</email>
+      </person>
+      <person firstname="Bob" lastname="Smith">
+        <email>bob@smith.com</email>
+      </person>
+    </opt>
+
+    {
+      '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<KeyAttr>
+option):
+
+    <opt>
+      <person key="jsmith" firstname="Joe" lastname="Smith" />
+      <person key="tsmith" firstname="Tom" lastname="Smith" />
+      <person key="jbloggs" firstname="Joe" lastname="Bloggs" />
+    </opt>
+
+    {
+      'person' => {
+                    'jbloggs' => {
+                                   'firstname' => 'Joe',
+                                   'lastname' => 'Bloggs'
+                                 },
+                    'tsmith' => {
+                                  'firstname' => 'Tom',
+                                  'lastname' => 'Smith'
+                                },
+                    'jsmith' => {
+                                  'firstname' => 'Joe',
+                                  'lastname' => 'Smith'
+                                }
+                  }
+    }
+
+
+The <anon> tag can be used to form anonymous arrays:
+
+    <opt>
+      <head><anon>Col 1</anon><anon>Col 2</anon><anon>Col 3</anon></head>
+      <data><anon>R1C1</anon><anon>R1C2</anon><anon>R1C3</anon></data>
+      <data><anon>R2C1</anon><anon>R2C2</anon><anon>R2C3</anon></data>
+      <data><anon>R3C1</anon><anon>R3C2</anon><anon>R3C3</anon></data>
+    </opt>
+
+    {
+      '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:
+
+    <opt>
+      <anon><anon>Col 1</anon><anon>Col 2</anon></anon>
+      <anon><anon>R1C1</anon><anon>R1C2</anon></anon>
+      <anon><anon>R2C1</anon><anon>R2C2</anon></anon>
+    </opt>
+
+    [
+      [ '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<ContentKey> option):
+
+  <opt>
+    <one>first</one>
+    <two attr="value">second</two>
+  </opt>
+
+  {
+    '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<XML::Simple> 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<http://perl-xml.sourceforge.net/faq/>
+
+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<XML::Twig> and more standards based DOM implementations - preferably one with
+XPath support.
+
+
+=head1 SEE ALSO
+
+B<XML::Simple> requires either L<XML::Parser> or L<XML::SAX>.
+
+To generate documents with namespaces, L<XML::NamespaceSupport> is required.
+
+The optional caching functions require L<Storable>.
+
+Answers to Frequently Asked Questions about XML::Simple are bundled with this
+distribution as: L<XML::Simple::FAQ>
+
+=head1 COPYRIGHT 
+
+Copyright 1999-2004 Grant McLean E<lt>grantm@cpan.orgE<gt>
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself. 
+
+=cut
+
+
diff --git a/lib/XML/Simple/FAQ.pod b/lib/XML/Simple/FAQ.pod
new file mode 100644 (file)
index 0000000..0aa52e8
--- /dev/null
@@ -0,0 +1,646 @@
+package XML::Simple::FAQ;
+1;
+
+__END__
+
+=head1 Frequently Asked Questions about XML::Simple
+
+
+=head1 Basics
+
+
+=head2 What is XML::Simple designed to be used for?
+
+XML::Simple is a Perl module that was originally developed as a tool for
+reading and writing configuration data in XML format.  You can use it for
+many other purposes that involve storing and retrieving structured data in
+XML.
+
+You might also find XML::Simple a good starting point for playing with XML
+from Perl.  It doesn't have a steep learning curve and if you outgrow its
+capabilities there are plenty of other Perl/XML modules to 'step up' to.
+
+
+=head2 Why store configuration data in XML anyway?
+
+The many advantages of using XML format for configuration data include:
+
+=over 4
+
+=item *
+
+Using existing XML parsing tools requires less development time, is easier
+and more robust than developing your own config file parsing code
+
+=item *
+
+XML can represent relationships between pieces of data, such as nesting of
+sections to arbitrary levels (not easily done with .INI files for example)
+
+=item *
+
+XML is basically just text, so you can easily edit a config file (easier than
+editing a Win32 registry)
+
+=item *
+
+XML provides standard solutions for handling character sets and encoding
+beyond basic ASCII (important for internationalization)
+
+=item *
+
+If it becomes necessary to change your configuration file format, there are
+many tools available for performing transformations on XML files
+
+=item *
+
+XML is an open standard (the world does not need more proprietary binary
+file formats)
+
+=item *
+
+Taking the extra step of developing a DTD allows the format of configuration
+files to be validated before your program reads them (not directly supported
+by XML::Simple)
+
+=item *
+
+Combining a DTD with a good XML editor can give you a GUI config editor for
+minimal coding effort
+
+=back
+
+
+=head2 What isn't XML::Simple good for?
+
+The main limitation of XML::Simple is that it does not work with 'mixed
+content' (see the next question).  If you consider your XML files contain
+marked up text rather than structured data, you should probably use another
+module.
+
+If you are working with very large XML files, XML::Simple's approach of
+representing the whole file in memory as a 'tree' data structure may not be
+suitable.
+
+
+=head2 What is mixed content?
+
+Consider this example XML:
+
+  <document>
+    <para>This is <em>mixed</em> content.</para>
+  </document>
+
+This is said to be mixed content, because the E<lt>paraE<gt> element contains
+both character data (text content) and nested elements.
+
+Here's some more XML:
+
+  <person>
+    <first_name>Joe</first_name>
+    <last_name>Bloggs</last_name>
+    <dob>25-April-1969</dob>
+  </person>
+
+This second example is not generally considered to be mixed content.  The
+E<lt>first_nameE<gt>, E<lt>last_nameE<gt> and E<lt>dobE<gt> elements contain
+only character data and the  E<lt>personE<gt> element contains only nested
+elements.  (Note: Strictly speaking, the whitespace between the nested
+elements is character data, but it is ignored by XML::Simple).
+
+
+=head2 Why doesn't XML::Simple handle mixed content?
+
+Because if it did, it would no longer be simple :-)
+
+Seriously though, there are plenty of excellent modules that allow you to
+work with mixed content in a variety of ways.  Handling mixed content
+correctly is not easy and by ignoring these issues, XML::Simple is able to
+present an API without a steep learning curve.
+
+
+=head2 Which Perl modules do handle mixed content?
+
+Every one of them except XML::Simple :-)
+
+If you're looking for a recommendation, I'd suggest you look at the Perl-XML
+FAQ at:
+
+  http://perl-xml.sourceforge.net/faq/
+
+
+=head1 Installation
+
+
+=head2 How do I install XML::Simple?
+
+If you're running ActiveState Perl, you've probably already got XML::Simple 
+(although you may want to upgrade to version 1.09 or better for SAX support).
+
+If you do need to install XML::Simple, you'll need to install an XML parser
+module first.  Install either XML::Parser (which you may have already) or
+XML::SAX.  If you install both, XML::SAX will be used by default.
+
+Once you have a parser installed ...
+
+On Unix systems, try:
+
+  perl -MCPAN -e 'install XML::Simple'
+
+If that doesn't work, download the latest distribution from
+ftp://ftp.cpan.org/pub/CPAN/authors/id/G/GR/GRANTM , unpack it and run these
+commands:
+
+  perl Makefile.PL
+  make
+  make test
+  make install
+
+On Win32, if you have a recent build of ActiveState Perl (618 or better) try
+this command:
+
+  ppm install XML::Simple
+
+If that doesn't work, you really only need the Simple.pm file, so extract it
+from the .tar.gz file (eg: using WinZIP) and save it in the \site\lib\XML 
+directory under your Perl installation (typically C:\Perl).
+
+
+=head2 I'm trying to install XML::Simple and 'make test' fails
+
+Is the directory where you've unpacked XML::Simple mounted from a file server
+using NFS, SMB or some other network file sharing?  If so, that may cause
+errors in the the following test scripts:
+
+  3_Storable.t
+  4_MemShare.t
+  5_MemCopy.t
+
+The test suite is designed to exercise the boundary conditions of all
+XML::Simple's functionality and these three scripts exercise the caching
+functions.  If XML::Simple is asked to parse a file for which it has a cached
+copy of a previous parse, then it compares the timestamp on the XML file with
+the timestamp on the cached copy.  If the cached copy is *newer* then it will
+be used.  If the cached copy is older or the same age then the file is
+re-parsed.  The test scripts will get confused by networked filesystems if
+the workstation and server system clocks are not synchronised (to the
+second).
+
+If you get an error in one of these three test scripts but you don't plan to
+use the caching options (they're not enabled by default), then go right ahead
+and run 'make install'.  If you do plan to use caching, then try unpacking
+the distribution on local disk and doing the build/test there.
+
+It's probably not a good idea to use the caching options with networked
+filesystems in production.  If the file server's clock is ahead of the local
+clock, XML::Simple will re-parse files when it could have used the cached
+copy.  However if the local clock is ahead of the file server clock and a
+file is changed immediately after it is cached, the old cached copy will be
+used.
+
+Is one of the three test scripts (above) failing but you're not running on
+a network filesystem?  Are you running Win32?  If so, you may be seeing a bug
+in Win32 where writes to a file do not affect its modfication timestamp.
+
+If none of these scenarios match your situation, please confirm you're
+running the latest version of XML::Simple and then email the output of
+'make test' to me at grantm@cpan.org
+
+=head2 Why is XML::Simple so slow?
+
+If you find that XML::Simple is very slow reading XML, the most likely reason
+is that you have XML::SAX installed but no additional SAX parser module.  The
+XML::SAX distribution includes an XML parser written entirely in Perl.  This is
+very portable but not very fast.  For better performance install either
+XML::SAX::Expat or XML::LibXML.
+
+
+=head1 Usage
+
+=head2 How do I use XML::Simple?
+
+If you had an XML document called /etc/appconfig/foo.xml you could 'slurp' it
+into a simple data structure (typically a hashref) with these lines of code:
+
+  use XML::Simple;
+
+  my $config = XMLin('/etc/appconfig/foo.xml');
+
+The XMLin() function accepts options after the filename.
+
+
+=head2 There are so many options, which ones do I really need to know about?
+
+Although you can get by without using any options, you shouldn't even
+consider using XML::Simple in production until you know what these two
+options do:
+
+=over 4
+
+=item *
+
+forcearray
+
+=item *
+
+keyattr
+
+=back
+
+The reason you really need to read about them is because the default values
+for these options will trip you up if you don't.  Although everyone agrees
+that these defaults are not ideal, there is not wide agreement on what they
+should be changed to.  The answer therefore is to read about them (see below)
+and select values which are right for you.
+
+
+=head2 What is the forcearray option all about?
+
+Consider this XML in a file called ./person.xml:
+
+  <person>
+    <first_name>Joe</first_name>
+    <last_name>Bloggs</last_name>
+    <hobbie>bungy jumping</hobbie>
+    <hobbie>sky diving</hobbie>
+    <hobbie>knitting</hobbie>
+  </person>
+
+You could read it in with this line:
+
+  my $person = XMLin('./person.xml');
+
+Which would give you a data structure like this:
+
+  $person = {
+    'first_name' => 'Joe',
+    'last_name'  => 'Bloggs',
+    'hobbie'     => [ 'bungy jumping', 'sky diving', 'knitting' ]
+  };
+
+The E<lt>first_nameE<gt> and E<lt>last_nameE<gt> elements are represented as
+simple scalar values which you could refer to like this:
+
+  print "$person->{first_name} $person->{last_name}\n";
+
+The E<lt>hobbieE<gt> elements are represented as an array - since there is
+more than one.  You could refer to the first one like this:
+
+  print $person->{hobbie}->[0], "\n";
+
+Or the whole lot like this:
+
+  print join(', ', @{$person->{hobbie}} ), "\n";
+
+The catch is, that these last two lines of code will only work for people
+who have more than one hobbie.  If there is only one E<lt>hobbieE<gt>
+element, it will be represented as a simple scalar (just like
+E<lt>first_nameE<gt> and E<lt>last_nameE<gt>).  Which might lead you to write
+code like this:
+
+  if(ref($person->{hobbie})) {
+    print join(', ', @{$person->{hobbie}} ), "\n";
+  }
+  else {
+    print $person->{hobbie}, "\n";
+  }
+
+Don't do that.
+
+One alternative approach is to set the forcearray option to a true value:
+
+  my $person = XMLin('./person.xml', forcearray => 1);
+
+Which will give you a data structure like this:
+
+  $person = {
+    'first_name' => [ 'Joe' ],
+    'last_name'  => [ 'Bloggs' ],
+    'hobbie'     => [ 'bungy jumping', 'sky diving', 'knitting' ]
+  };
+
+Then you can use this line to refer to all the list of hobbies even if there
+was only one:
+
+  print join(', ', @{$person->{hobbie}} ), "\n";
+
+The downside of this approach is that the E<lt>first_nameE<gt> and
+E<lt>last_nameE<gt> elements will also always be represented as arrays even
+though there will never be more than one:
+
+  print "$person->{first_name}->[0] $person->{last_name}->[0]\n";
+
+This might be OK if you change the XML to use attributes for things that
+will always be singular and nested elements for things that may be plural:
+
+  <person first_name="Jane" last_name="Bloggs">
+    <hobbie>motorcycle maintenance</hobbie>
+  </person>
+
+On the other hand, if you prefer not to use attributes, then you could
+specify that any E<lt>hobbieE<gt> elements should always be represented as
+arrays and all other nested elements should be simple scalar values unless
+there is more than one:
+
+  my $person = XMLin('./person.xml', forcearray => [ 'hobbie' ]);
+
+The forcearray option accepts a list of element names which should always
+be forced to an array representation:
+
+  forcearray => [ qw(hobbie qualification childs_name) ]
+
+See the XML::Simple manual page for more information.
+
+
+=head2 What is the keyattr option all about?
+
+Consider this sample XML:
+
+  <catalog>
+    <part partnum="1842334" desc="High pressure flange" price="24.50" />
+    <part partnum="9344675" desc="Threaded gasket"      price="9.25" />
+    <part partnum="5634896" desc="Low voltage washer"   price="12.00" />
+  </catalog>
+
+You could slurp it in with this code:
+
+  my $catalog = XMLin('./catalog.xml');
+
+Which would return a data structure like this:
+
+  $catalog = {
+      'part' => [
+          {
+            'partnum' => '1842334',
+            'desc'    => 'High pressure flange',
+            'price'   => '24.50'
+          },
+          {
+            'partnum' => '9344675',
+            'desc'    => 'Threaded gasket',
+            'price'   => '9.25'
+          },
+          {
+            'partnum' => '5634896',
+            'desc'    => 'Low voltage washer',
+            'price'   => '12.00'
+          }
+      ]
+  };
+
+Then you could access the description of the first part in the catalog
+with this code:
+
+  print $catalog->{part}->[0]->{desc}, "\n";
+
+However, if you wanted to access the description of the part with the
+part number of "9344675" then you'd have to code a loop like this:
+
+  foreach my $part (@{$catalog->{part}}) {
+    if($part->{partnum} eq '9344675') {
+      print $part->{desc}, "\n";
+      last;
+    }
+  }
+
+The knowledge that each E<lt>partE<gt> element has a unique partnum attribute
+allows you to eliminate this search.  You can pass this knowledge on to
+XML::Simple like this:
+
+  my $catalog = XMLin($xml, keyattr => ['partnum']);
+
+Which will return a data structure like this:
+
+  $catalog = {
+    'part' => {
+      '5634896' => { 'desc' => 'Low voltage washer',   'price' => '12.00' },
+      '1842334' => { 'desc' => 'High pressure flange', 'price' => '24.50' },
+      '9344675' => { 'desc' => 'Threaded gasket',      'price' => '9.25'  }
+    }
+  };
+
+XML::Simple has been able to transform $catalog->{part} from an arrayref to
+a hashref (keyed on partnum).  This transformation is called 'array folding'.
+
+Through the use of array folding, you can now index directly to the
+description of the part you want:
+
+  print $catalog->{part}->{9344675}->{desc}, "\n";
+
+The 'keyattr' option also enables array folding when the unique key is in a
+nested element rather than an attribute.  eg:
+
+  <catalog>
+    <part>
+      <partnum>1842334</partnum>
+      <desc>High pressure flange</desc>
+      <price>24.50</price>
+    </part>
+    <part>
+      <partnum>9344675</partnum>
+      <desc>Threaded gasket</desc>
+      <price>9.25</price>
+    </part>
+    <part>
+      <partnum>5634896</partnum>
+      <desc>Low voltage washer</desc>
+      <price>12.00</price>
+    </part>
+  </catalog>
+
+See the XML::Simple manual page for more information.
+
+
+=head2 So what's the catch with 'keyattr'?
+
+One thing to watch out for is that you might get array folding even if you
+don't supply the keyattr option.  The default value for this option is:
+
+  [ 'name', 'key', 'id']
+
+Which means if your XML elements have a 'name', 'key' or 'id' attribute (or
+nested element) then they may get folded on those values.  This means that
+you can take advantage of array folding simply through careful choice of
+attribute names.  On the hand, if you really don't want array folding at all,
+you'll need to set 'key attr to an empty list:
+
+  my $ref = XMLin($xml, keyattr => []);
+
+A second 'gotcha' is that array folding only works on arrays.  That might
+seem obvious, but if there's only one record in your XML and you didn't set
+the 'forcearray' option then it won't be represented as an array and
+consequently won't get folded into a hash.  The moral is that if you're
+using array folding, you should always turn on the forcearray option.
+
+You probably want to be as specific as you can be too.  For instance, the
+safest way to parse the E<lt>catalogE<gt> example above would be:
+
+  my $catalog = XMLin($xml, keyattr => { part => 'partnum'},
+                            forcearray => ['part']);
+
+By using the hashref for keyattr, you can specify that only E<lt>partE<gt>
+elements should be folded on the 'partnum' attribute (and that the
+E<lt>partE<gt> elements should not be folded on any other attribute).
+
+By supplying a list of element names for forcearray, you're ensuring that
+folding will work even if there's only one E<lt>partE<gt>.  You're also
+ensuring that if the 'partnum' unique key is supplied in a nested element
+then that element won't get forced to an array too.
+
+
+=head2 How do I know what my data structure should look like?
+
+The rules are fairly straightforward:
+
+=over 4
+
+=item *
+
+each element gets represented as a hash
+
+=item *
+
+unless it contains only text, in which case it'll be a simple scalar value
+
+=item *
+
+or unless there's more than one element with the same name, in which case
+they'll be represented as an array
+
+=item *
+
+unless you've got array folding enabled, in which case they'll be folded into
+a hash
+
+=item *
+
+empty elements (no text contents B<and> no attributes) will either be
+represented as an empty hash, an empty string or undef - depending on the value
+of the 'suppressempty' option.
+
+=back
+
+If you're in any doubt, use Data::Dumper, eg:
+
+  use XML::Simple;
+  use Data::Dumper;
+  
+  my $ref = XMLin($xml);
+
+  print Dumper($ref);
+
+
+=head2 I'm getting 'Use of uninitialized value' warnings
+
+You're probably trying to index into a non-existant hash key - try
+Data::Dumper.
+
+
+=head2 I'm getting a 'Not an ARRAY reference' error
+
+Something that you expect to be an array is not.  The two most likely causes
+are that you forgot to use 'forcearray' or that the array got folded into a
+hash - try Data::Dumper.
+
+
+=head2 I'm getting a 'No such array field' error
+
+Something that you expect to be a hash is actually an array.  Perhaps array
+folding failed because one element was missing the key attribute - try
+Data::Dumper.
+
+
+=head2 I'm getting an 'Out of memory' error
+
+Something in the data structure is not as you expect and Perl may be trying
+unsuccessfully to autovivify things - try Data::Dumper.
+
+If you're already using Data::Dumper, try calling Dumper() immediately after
+XMLin() - ie: before you attempt to access anything in the data structure.
+
+
+=head2 My element order is getting jumbled up
+
+If you read an XML file with XMLin() and then write it back out with
+XMLout(), the order of the elements will likely be different.  (However, if
+you read the file back in with XMLin() you'll get the same Perl data
+structure).
+
+The reordering happens because XML::Simple uses hashrefs to store your data
+and Perl hashes do not really have any order.
+
+It is possible that a future version of XML::Simple will use Tie::IxHash
+to store the data in hashrefs which do retain the order.  However this will
+not fix all cases of element order being lost.
+
+If your application really is sensitive to element order, don't use
+XML::Simple (and don't put order-sensitive values in attributes).
+
+
+=head2 XML::Simple turns nested elements into attributes
+
+If you read an XML file with XMLin() and then write it back out with
+XMLout(), some data which was originally stored in nested elements may end up
+in attributes.  (However, if you read the file back in with XMLin() you'll
+get the same Perl data structure).
+
+There are a number of ways you might handle this:
+
+=over 4
+
+=item *
+
+use the 'forcearray' option with XMLin()
+
+=item *
+
+use the 'noattr' option with XMLout()
+
+=item *
+
+live with it
+
+=item *
+
+don't use XML::Simple
+
+=back
+
+
+=head2 Why does XMLout() insert E<lt>nameE<gt> elements (or attributes)?
+
+Try setting keyattr => [].
+
+When you call XMLin() to read XML, the 'keyattr' option controls whether arrays
+get 'folded' into hashes.  Similarly, when you call XMLout(), the 'keyattr'
+option controls whether hashes get 'unfolded' into arrays.  As described above,
+'keyattr' is enabled by default.
+
+=head2 Why are empty elements represented as empty hashes?
+
+An element is always represented as a hash unless it contains only text, in
+which case it is represented as a scalar string.
+
+If you would prefer empty elements to be represented as empty strings or the
+undefined value, set the 'suppressempty' option to '' or undef respectively.
+
+=head2 Why is ParserOpts deprecated?
+
+The C<ParserOpts> option is a remnant of the time when XML::Simple only worked
+with the XML::Parser API.  Its value is completely ignored if you're using a
+SAX parser, so writing code which relied on it would bar you from taking
+advantage of SAX.
+
+Even if you are using XML::Parser, it is seldom necessary to pass options to
+the parser object.  A number of people have written to say they use this option
+to set XML::Parser's C<ProtocolEncoding> option.  Don't do that, it's wrong,
+Wrong, WRONG!  Fix the XML document so that it's well-formed and you won't have
+a problem.
+
+Having said all of that, as long as XML::Simple continues to support the
+XML::Parser API, this option will not be removed.  There are currently no plans
+to remove support for the XML::Parser API.
+
+=cut
+
+
diff --git a/maketest b/maketest
new file mode 100644 (file)
index 0000000..8e644ab
--- /dev/null
+++ b/maketest
@@ -0,0 +1,92 @@
+#!perl -w
+#############################################################################
+#
+# If your platform/installation does not support make test, you can try this:
+#
+#    perl maketest
+#
+#############################################################################
+
+use strict;
+use File::Spec;
+use Test::Harness qw(&runtests $verbose);
+
+$verbose = 0;
+
+
+#############################################################################
+# Are we in the right directory?  If not, the pathname component of $0
+# must be pointing to it or we wouldn't be running this script.
+#
+
+unless(-f 'MANIFEST') {
+  my($script_name, $script_dir) = File::Basename::fileparse($0);
+  if($script_dir) {
+    chdir($script_dir) || die "chdir($script_dir): $!";
+  }
+}
+
+
+#############################################################################
+# Confirm distribution is complete (read MANIFEST file without assuming it
+# has been converted to platform's native text format).
+# Build up a list of test files as we go.
+#
+
+my @tests = ();
+{
+  open(MNFST, 'MANIFEST') || die "open(MANIFEST): $!";
+  local($/) = undef;
+  foreach(split(/[\r\n]+/, <MNFST>)) {
+    next unless(/\S/);
+    my $src_file = File::Spec->catfile(split('/'));
+    (-f $src_file  ) || die "Could not find expected file: $src_file";
+    push @tests, $src_file if($src_file =~ /^t\b.*\.t$/);
+  }
+  close(MNFST);
+}
+
+print "XML::Distribution appears complete\n";
+
+
+#############################################################################
+# Build and populate what we need of blib (the build library)
+#
+
+my $path = 'blib';
+(-d $path) || mkdir($path, 0777) || die "mkdir($path): $!";
+
+$path = File::Spec->catdir($path, 'lib');
+(-d $path) || mkdir($path, 0777) || die "mkdir($path): $!";
+
+$path = File::Spec->catdir($path, 'XML');
+(-d $path) || mkdir($path, 0777) || die "mkdir($path): $!";
+
+$path = File::Spec->catfile($path, 'Simple.pm');
+unless(-f $path) {
+  open(MOD, 'Simple.pm') || die "open(Simple.pm): $!";
+  {
+    local($/) = undef;
+    my $module = <MOD>;
+    close(MOD);
+    open(MOD, ">$path") || die "open($path): $!";
+    print MOD $module;
+    close(MOD);
+  }
+  print "Created $path\n";
+}
+
+
+#############################################################################
+# Run the tests
+#
+
+print "Running tests...\n";
+
+unshift @INC, 'blib/lib';
+
+@tests = @ARGV if(@ARGV);
+
+runtests @tests;
+
+
diff --git a/t/0_Config.t b/t/0_Config.t
new file mode 100644 (file)
index 0000000..ebb0377
--- /dev/null
@@ -0,0 +1,64 @@
+# $Id: 0_Config.t,v 1.6 2003/05/20 08:50:10 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More tests => 1;
+
+
+# Build up a list of installed modules
+
+my @mod_list = qw(XML::Simple Storable XML::Parser XML::SAX);
+
+
+# If XML::SAX is installed, add a list of installed SAX parsers
+
+eval " use XML::SAX; ";
+my $default_parser = '';
+unless($@) {
+  push @mod_list, 'XML::NamespaceSupport';
+  push @mod_list, map { $_->{Name} } @{XML::SAX->parsers()};
+  $default_parser = ref(XML::SAX::ParserFactory->parser());
+}
+
+
+# Extract the version number from each module
+
+my(%version);
+foreach my $module (@mod_list) {
+  eval " require $module; ";
+  unless($@) {
+    no strict 'refs';
+    $version{$module} = ${$module . '::VERSION'} || "Unknown";
+  }
+}
+
+$default_parser = 'XML::Parser' if(!$default_parser && $version{'XML::Parser'});
+
+
+# Add version number of the Perl binary
+
+eval ' use Config; $version{perl} = $Config{version} ';  # Should never fail
+if($@) {
+  $version{perl} = $];
+}
+unshift @mod_list, 'perl';
+
+
+# Check for preferred parser via environment setting
+
+my $preferred_parser = $ENV{XML_SIMPLE_PREFERRED_PARSER} || ' ';
+
+
+# Print details of installed modules on STDERR
+
+diag(sprintf("\r# %-30s %s\n", 'Package', 'Version'));
+foreach my $module (@mod_list) {
+  $version{$module} = 'Not Installed' unless(defined($version{$module}));
+  $version{$module} .= " (default parser)" if($module eq $default_parser);
+  $version{$module} .= " (preferred parser)" if($module eq $preferred_parser);
+  diag(sprintf(" %-30s %s\n", $module, $version{$module}));
+}
+
+# Housekeeping
+
+ok(1, "Dumped config");
diff --git a/t/1_XMLin.t b/t/1_XMLin.t
new file mode 100644 (file)
index 0000000..b5b6d03
--- /dev/null
@@ -0,0 +1,1509 @@
+# $Id: 1_XMLin.t,v 1.28 2007/08/15 10:36:48 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use IO::File;
+use File::Spec;
+
+$^W = 1;
+
+
+# Initialise filenames and check they're there
+
+my $XMLFile = File::Spec->catfile('t', 'test1.xml');  # t/test1.xml
+
+unless(-e $XMLFile) {
+  plan skip_all => 'Test data missing';
+}
+
+plan tests => 131;
+
+
+my $last_warning = '';
+
+$@ = '';
+eval "use XML::Simple;";
+is($@, '', 'Module compiled OK');
+my $version = 'unknown';
+if(open my $chg, '<Changes') {
+  while(<$chg>) {
+    last if ($version) = $_ =~ /^([\d\._]+) /;
+  }
+  close($chg);
+}
+unless($XML::Simple::VERSION eq $version) {
+  diag("Warning: XML::Simple::VERSION = $XML::Simple::VERSION (Changes version: $version)");
+}
+
+
+# Start by parsing an extremely simple piece of XML
+
+my $opt = XMLin(q(<opt name1="value1" name2="value2"></opt>));
+
+my $expected = {
+                name1 => 'value1',
+                name2 => 'value2',
+              };
+
+ok(1, "XMLin() didn't crash");
+ok(defined($opt), 'and it returned a value');
+is(ref($opt), 'HASH', 'and a hasref at that');
+is_deeply($opt, $expected, 'matches expectations (attributes)');
+
+
+# Now try a slightly more complex one that returns the same value
+
+$opt = XMLin(q(
+  <opt> 
+    <name1>value1</name1>
+    <name2>value2</name2>
+  </opt>
+));
+is_deeply($opt, $expected, 'same again with nested elements');
+
+
+# And something else that returns the same (line break included to pick up
+# missing /s bug)
+
+$opt = XMLin(q(<opt name1="value1"
+                    name2="value2" />));
+is_deeply($opt, $expected, 'attributes in empty element');
+
+
+# Try something with two lists of nested values 
+
+$opt = XMLin(q(
+  <opt> 
+    <name1>value1.1</name1>
+    <name1>value1.2</name1>
+    <name1>value1.3</name1>
+    <name2>value2.1</name2>
+    <name2>value2.2</name2>
+    <name2>value2.3</name2>
+  </opt>)
+);
+
+is_deeply($opt, {
+  name1 => [ 'value1.1', 'value1.2', 'value1.3' ],
+  name2 => [ 'value2.1', 'value2.2', 'value2.3' ],
+}, 'repeated child elements give arrays of scalars');
+
+
+# Now a simple nested hash
+
+$opt = XMLin(q(
+  <opt> 
+    <item name1="value1" name2="value2" />
+  </opt>)
+);
+
+is_deeply($opt, {
+  item => { name1 => 'value1', name2 => 'value2' }
+}, 'nested element gives hash');
+
+
+# Now a list of nested hashes
+
+$opt = XMLin(q(
+  <opt> 
+    <item name1="value1" name2="value2" />
+    <item name1="value3" name2="value4" />
+  </opt>)
+);
+is_deeply($opt, {
+  item => [
+            { name1 => 'value1', name2 => 'value2' },
+            { name1 => 'value3', name2 => 'value4' }
+         ]
+}, 'repeated child elements give list of hashes');
+
+
+# Now a list of nested hashes transformed into a hash using default key names
+
+my $string = q(
+  <opt> 
+    <item name="item1" attr1="value1" attr2="value2" />
+    <item name="item2" attr1="value3" attr2="value4" />
+  </opt>
+);
+my $target = {
+  item => {
+            item1 => { attr1 => 'value1', attr2 => 'value2' },
+            item2 => { attr1 => 'value3', attr2 => 'value4' }
+         }
+};
+$opt = XMLin($string);
+is_deeply($opt, $target, "array folded on default key 'name'");
+
+
+# Same thing left as an array by suppressing default key names
+
+$target = {
+  item => [
+            {name => 'item1', attr1 => 'value1', attr2 => 'value2' },
+            {name => 'item2', attr1 => 'value3', attr2 => 'value4' }
+         ]
+};
+my @cont_key = (contentkey => '-content');
+$opt = XMLin($string, keyattr => [], @cont_key);
+is_deeply($opt, $target, 'not folded when keyattr turned off');
+
+
+# Same again with alternative key suppression
+
+$opt = XMLin($string, keyattr => {}, @cont_key);
+is_deeply($opt, $target, 'still works when keyattr is empty hash');
+
+
+# Try the other two default key attribute names
+
+$opt = XMLin(q(
+  <opt> 
+    <item key="item1" attr1="value1" attr2="value2" />
+    <item key="item2" attr1="value3" attr2="value4" />
+  </opt>
+), @cont_key);
+is_deeply($opt, {
+  item => {
+            item1 => { attr1 => 'value1', attr2 => 'value2' },
+            item2 => { attr1 => 'value3', attr2 => 'value4' }
+         }
+}, "folded on default key 'key'");
+
+
+$opt = XMLin(q(
+  <opt> 
+    <item id="item1" attr1="value1" attr2="value2" />
+    <item id="item2" attr1="value3" attr2="value4" />
+  </opt>
+), @cont_key);
+is_deeply($opt, {
+  item => {
+            item1 => { attr1 => 'value1', attr2 => 'value2' },
+            item2 => { attr1 => 'value3', attr2 => 'value4' }
+         }
+}, "folded on default key 'id'");
+
+
+# Similar thing using non-standard key names
+
+my $xml = q(
+  <opt> 
+    <item xname="item1" attr1="value1" attr2="value2" />
+    <item xname="item2" attr1="value3" attr2="value4" />
+  </opt>);
+
+$target = {
+  item => {
+            item1 => { attr1 => 'value1', attr2 => 'value2' },
+            item2 => { attr1 => 'value3', attr2 => 'value4' }
+         }
+};
+
+$opt = XMLin($xml, keyattr => [qw(xname)], @cont_key);
+is_deeply($opt, $target, "folded on non-default key 'xname'");
+
+
+# And with precise element/key specification
+
+$opt = XMLin($xml, keyattr => { 'item' => 'xname' }, @cont_key);
+is_deeply($opt, $target, 'same again but keyattr set with hash');
+
+
+# Same again but with key field further down the list
+
+$opt = XMLin($xml, keyattr => [qw(wibble xname)], @cont_key);
+is_deeply($opt, $target, 'keyattr as array with value in second position');
+
+
+# Same again but with key field supplied as scalar
+
+$opt = XMLin($xml, keyattr => qw(xname), @cont_key);
+is_deeply($opt, $target, 'keyattr as scalar');
+
+
+# Same again but with mixed-case option name
+
+$opt = XMLin($xml, KeyAttr => qw(xname), @cont_key);
+is_deeply($opt, $target, 'KeyAttr as scalar');
+
+
+# Same again but with underscores in option name
+
+$opt = XMLin($xml, key_attr => qw(xname), @cont_key);
+is_deeply($opt, $target, 'key_attr as scalar');
+
+
+# Weird variation, not exactly what we wanted but it is what we expected 
+# given the current implementation and we don't want to break it accidently
+
+$xml = q(
+<opt>
+  <item id="one" value="1" name="a" />
+  <item id="two" value="2" />
+  <item id="three" value="3" />
+</opt>
+);
+
+$target = { item => {
+    'three' => { 'value' => 3 },
+    'a'     => { 'value' => 1, 'id' => 'one' },
+    'two'   => { 'value' => 2 }
+  }
+};
+
+$opt = XMLin($xml, @cont_key);
+is_deeply($opt, $target, 'fold same array on two different keys');
+
+
+# Or somewhat more as one might expect
+
+$target = { item => {
+    'one'   => { 'value' => '1', 'name' => 'a' },
+    'two'   => { 'value' => '2' },
+    'three' => { 'value' => '3' },
+  }
+};
+$opt = XMLin($xml, keyattr => { 'item' => 'id' }, @cont_key);
+is_deeply($opt, $target, 'same again but with priority switch');
+
+
+# Now a somewhat more complex test of targetting folding
+
+$xml = q(
+<opt>
+  <car license="SH6673" make="Ford" id="1">
+    <option key="1" pn="6389733317-12" desc="Electric Windows"/>
+    <option key="2" pn="3735498158-01" desc="Leather Seats"/>
+    <option key="3" pn="5776155953-25" desc="Sun Roof"/>
+  </car>
+  <car license="LW1804" make="GM"   id="2">
+    <option key="1" pn="9926543-1167" desc="Steering Wheel"/>
+  </car>
+</opt>
+);
+
+$target = {
+  'car' => {
+    'LW1804' => {
+      'id' => 2,
+      'make' => 'GM',
+      'option' => {
+         '9926543-1167' => { 'key' => 1, 'desc' => 'Steering Wheel' }
+      }
+    },
+    'SH6673' => {
+      'id' => 1,
+      'make' => 'Ford',
+      'option' => {
+         '6389733317-12' => { 'key' => 1, 'desc' => 'Electric Windows' },
+         '3735498158-01' => { 'key' => 2, 'desc' => 'Leather Seats' },
+         '5776155953-25' => { 'key' => 3, 'desc' => 'Sun Roof' }
+      }
+    }
+  }
+};
+
+$opt = XMLin($xml, forcearray => 1,
+  keyattr => { 'car' => 'license', 'option' => 'pn' }, @cont_key);
+is_deeply($opt, $target, 'folded on multi-key keyattr hash');
+
+
+# Now try leaving the keys in place
+
+$target = {
+  'car' => {
+    'LW1804' => {
+      'id' => 2,
+      'make' => 'GM',
+      'option' => {
+         '9926543-1167' => { 'key' => 1, 'desc' => 'Steering Wheel',
+                             '-pn' => '9926543-1167' }
+      },
+      license => 'LW1804'
+    },
+    'SH6673' => {
+      'id' => 1,
+      'make' => 'Ford',
+      'option' => {
+         '6389733317-12' => { 'key' => 1, 'desc' => 'Electric Windows',
+                              '-pn' => '6389733317-12' },
+         '3735498158-01' => { 'key' => 2, 'desc' => 'Leather Seats',
+                              '-pn' => '3735498158-01' },
+         '5776155953-25' => { 'key' => 3, 'desc' => 'Sun Roof',
+                              '-pn' => '5776155953-25' }
+      },
+      license => 'SH6673'
+    }
+  }
+};
+$opt = XMLin($xml, forcearray => 1, keyattr => { 'car' => '+license', 'option' => '-pn' }, @cont_key);
+is_deeply($opt, $target, "same again but with '+' prefix to copy keys");
+
+
+# Confirm the stringifying references bug is fixed
+
+$xml = q(
+  <opt>
+    <item>
+      <name><firstname>Bob</firstname></name>
+      <age>21</age>
+    </item>
+    <item>
+      <name><firstname>Kate</firstname></name>
+      <age>22</age>
+    </item>
+  </opt>);
+
+$target = {
+  item => [
+    { age => '21', name => { firstname => 'Bob'} },
+    { age => '22', name => { firstname => 'Kate'} },
+  ]
+};
+
+{
+  local($SIG{__WARN__}) = \&warn_handler;
+
+  $last_warning = '';
+  $opt = XMLin($xml, @cont_key);
+  is_deeply($opt, $target, "did not fold on default key with non-scalar value");
+  is($last_warning, '', 'no warning issued');
+
+  $last_warning = '';
+  $opt = XMLin($xml, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $target, "did not fold on specific key with non-scalar value");
+  isnt($last_warning, '', 'warning issued as expected');
+  like($last_warning, 
+    qr{<item> element has non-scalar 'name' key attribute},
+    'text in warning is correct'
+  );
+
+  $last_warning = '';
+  $opt = XMLin($xml, keyattr => [ 'name' ], @cont_key);
+  is_deeply($opt, $target, "same again but with keyattr as array");
+  isnt($last_warning, '', 'warning issued as expected');
+  like($last_warning, 
+    qr{<item> element has non-scalar 'name' key attribute},
+    'text in warning is correct'
+  );
+
+  $last_warning = '';
+  local($^W) = 0;
+  $opt = XMLin($xml, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $target, "did not fold on specific key with non-scalar value");
+  is($last_warning, '', 'no warning issued (as expected)');
+
+  $last_warning = '';
+  $^W = 1;
+  my $xitems = q(<opt>
+    <item name="color">red</item>
+    <item name="mass">heavy</item>
+    <item nime="disposition">ornery</item>
+  </opt>);
+  my $items = {
+    'item' => [
+      { 'name' => 'color',       'content' => 'red',    },
+      { 'name' => 'mass',        'content' => 'heavy',  },
+      { 'nime' => 'disposition', 'content' => 'ornery', }
+    ]
+  };
+  $opt = XMLin($xitems, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $items, "did not fold when element missing key attribute");
+  like($last_warning, qr{Warning: <item> element has no 'name' key attribute},
+    'expected warning issued');
+
+  $last_warning = '';
+  $^W = 0;
+  $opt = XMLin($xitems, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $items, "same again");
+  is($last_warning, '', 'but with no warning this time');
+
+  $last_warning = '';
+  $^W = 1;
+  $xitems = q(<opt>
+    <item name="color">red</item>
+    <item name="mass">heavy</item>
+    <item name="disposition">ornery</item>
+    <item name="color">green</item>
+  </opt>);
+  $items = {
+    'item' => {
+      'color'       => 'green',
+      'mass'        => 'heavy',
+      'disposition' => 'ornery',
+    }
+  };
+  $opt = XMLin($xitems, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $items, "folded elements despite non-unique key attribute");
+  like($last_warning, qr{Warning: <item> element has non-unique value in 'name' key attribute: color},
+    'expected warning issued');
+
+  $last_warning = '';
+  $opt = XMLin($xitems, keyattr => [ 'name' ], @cont_key);
+  is_deeply($opt, $items, "same again but with keyattr as array");
+  like($last_warning, qr{Warning: <item> element has non-unique value in 'name' key attribute: color},
+    'expected warning issued');
+
+  $last_warning = '';
+  $^W = 0;
+  $opt = XMLin($xitems, keyattr => { item => 'name' }, @cont_key);
+  is_deeply($opt, $items, "same again");
+  is($last_warning, '', 'but with no warning this time');
+}
+
+
+# Make sure that the root element name is preserved if we ask for it
+
+$target = XMLin("<opt>$xml</opt>", forcearray => 1,
+                keyattr => { 'car' => '+license', 'option' => '-pn' },
+                @cont_key);
+
+$opt    = XMLin(      $xml,        forcearray => 1, keeproot => 1,
+                keyattr => { 'car' => '+license', 'option' => '-pn' }, 
+                @cont_key);
+
+is_deeply($opt, $target, 'keeproot option works');
+
+
+# confirm that CDATA sections parse correctly
+
+$xml = q{<opt><cdata><![CDATA[<greeting>Hello, world!</greeting>]]></cdata></opt>};
+$opt = XMLin($xml, @cont_key);
+is_deeply($opt, {
+  'cdata' => '<greeting>Hello, world!</greeting>'
+}, 'CDATA section parsed correctly');
+
+$xml = q{<opt><x><![CDATA[<y>one</y>]]><![CDATA[<y>two</y>]]></x></opt>};
+$opt = XMLin($xml, @cont_key);
+is_deeply($opt, {
+  'x' => '<y>one</y><y>two</y>'
+}, 'CDATA section containing markup characters parsed correctly');
+
+
+# Try parsing a named external file
+
+$@ = '';
+$opt = eval{ XMLin($XMLFile); };
+is($@, '', "XMLin didn't choke on named external file");
+is_deeply($opt, {
+  location => 't/test1.xml'
+}, 'and contents parsed as expected');
+
+
+# Try parsing default external file (scriptname.xml in script directory)
+
+$@ = '';
+$opt = eval { XMLin(); };
+is($@, '', "XMLin didn't choke on un-named (default) external file");
+is_deeply($opt, {
+  location => 't/1_XMLin.xml'
+}, 'and contents parsed as expected');
+
+
+# Try parsing named file in a directory in the searchpath
+
+$@ = '';
+$opt = eval {
+  XMLin('test2.xml', searchpath => [
+    'dir1', 'dir2', File::Spec->catdir('t', 'subdir'), @cont_key
+  ] );
+
+};
+is($@, '', 'XMLin found file using searchpath');
+is_deeply($opt, {
+  location => 't/subdir/test2.xml' 
+}, 'and contents parsed as expected');
+
+
+# Ensure we get expected result if file does not exist
+
+$@ = '';
+$opt = undef;
+$opt = eval {
+  XMLin('bogusfile.xml', searchpath => 't' ); # should 'die'
+};
+is($opt, undef, 'XMLin choked on nonexistant file');
+like($@, qr/Could not find bogusfile.xml in/, 'with the expected message');
+
+
+# same again, but with no searchpath
+
+$@ = '';
+$opt = undef;
+$opt = eval { XMLin('bogusfile.xml'); };
+is($opt, undef, 'nonexistant file not found in current directory');
+like($@, qr/File does not exist: bogusfile.xml/, 'with the expected message');
+
+
+# Confirm searchpath is ignored if filename includes directory component
+
+$@ = '';
+$opt = undef;
+$opt = eval {
+  XMLin(File::Spec->catfile('subdir', 'test2.xml'), searchpath => 't' );
+};
+is($opt, undef, 'search path ignored when pathname supplied');
+
+
+# Try parsing from an IO::Handle 
+
+$@ = '';
+my $fh = new IO::File;
+$XMLFile = File::Spec->catfile('t', '1_XMLin.xml');  # t/1_XMLin.xml
+eval {
+  $fh->open($XMLFile) || die "$!";
+  $opt = XMLin($fh, @cont_key);
+};
+is($@, '', "XMLin didn't choke on an IO::File object");
+is($opt->{location}, 't/1_XMLin.xml', 'and it parsed the right file');
+
+
+# Try parsing from STDIN
+
+close(STDIN);
+$@ = '';
+eval {
+  open(STDIN, $XMLFile) || die "$!";
+  $opt = XMLin('-');
+};
+is($@, '', "XMLin didn't choke on STDIN ('-')");
+is($opt->{location}, 't/1_XMLin.xml', 'and data parsed correctly');
+
+
+# Confirm anonymous array handling works in general
+
+$xml = q{
+  <opt>
+    <row>
+      <anon>0.0</anon><anon>0.1</anon><anon>0.2</anon>
+    </row>
+    <row>
+      <anon>1.0</anon><anon>1.1</anon><anon>1.2</anon>
+    </row>
+    <row>
+      <anon>2.0</anon><anon>2.1</anon><anon>2.2</anon>
+    </row>
+  </opt>
+};
+
+$expected = {
+  row => [
+          [ '0.0', '0.1', '0.2' ],
+          [ '1.0', '1.1', '1.2' ],
+          [ '2.0', '2.1', '2.2' ]
+         ]
+};
+
+$opt = XMLin($xml, @cont_key);
+is_deeply($opt, $expected, 'anonymous arrays parsed correctly');
+
+# Confirm it still works with array folding disabled (was a bug)
+
+$opt = XMLin($xml, keyattr => [], @cont_key);
+is_deeply($opt, $expected, 'anonymous arrays parsed correctly');
+
+
+# Confirm anonymous array handling works in special top level case
+
+$opt = XMLin(q{
+  <opt>
+    <anon>one</anon>
+    <anon>two</anon>
+    <anon>three</anon>
+  </opt>
+}, @cont_key);
+is_deeply($opt, [
+  qw(one two three)
+], 'top level anonymous array returned arrayref');
+
+
+$opt = XMLin(q(
+  <opt>
+    <anon>1</anon>
+    <anon>
+      <anon>2.1</anon>
+      <anon>
+       <anon>2.2.1</anon>
+       <anon>2.2.2</anon>
+      </anon>
+    </anon>
+  </opt>
+), @cont_key);
+is_deeply($opt, [
+  1,
+  [
+   '2.1', [ '2.2.1', '2.2.2']
+  ]
+], 'nested anonymous arrays parsed correctly');
+
+
+# Check for the dreaded 'content' attribute
+
+$xml = q(
+  <opt>
+    <item attr="value">text</item>
+  </opt>
+);
+
+$opt = XMLin($xml);
+is_deeply($opt, {
+  item => {
+           content => 'text',
+           attr    => 'value' 
+          }
+}, "'content' key appears as expected");
+
+
+# And check that we can change its name if required
+
+$opt = XMLin($xml, contentkey => 'text_content');
+is_deeply($opt, {
+  item => {
+           text_content => 'text',
+           attr         => 'value'
+          }
+}, "'content' key successfully renamed to 'text'");
+
+
+# Check that it doesn't get screwed up by forcearray option
+
+$xml = q(<opt attr="value">text content</opt>);
+
+$opt = XMLin($xml, forcearray => 1);
+is_deeply($opt, {
+  'attr'    => 'value',
+  'content' => 'text content'
+}, "'content' key not munged by forcearray");
+
+
+# Test that we can force all text content to parse to hash values
+
+$xml = q(<opt><x>text1</x><y a="2">text2</y></opt>);
+$opt = XMLin($xml, forcecontent => 1);
+is_deeply($opt, {
+    'x' => {           'content' => 'text1' },
+    'y' => { 'a' => 2, 'content' => 'text2' }
+}, 'gratuitous use of content key works as expected');
+
+
+# And that this is compatible with changing the key name
+
+$opt = XMLin($xml, forcecontent => 1, contentkey => '0');
+is_deeply($opt, {
+    'x' => {           0 => 'text1' },
+    'y' => { 'a' => 2, 0 => 'text2' }
+}, "even when we change it's name to 'text'");
+
+
+# Confirm that spurious 'content' keys are *not* eliminated after array folding
+
+$xml = q(<opt><x y="one">First</x><x y="two">Second</x></opt>);
+$opt = XMLin($xml, forcearray => [ 'x' ], keyattr => {x => 'y'});
+is_deeply($opt, { 
+  x => {
+    one => { content => 'First'  },
+    two => { content => 'Second' },
+  }
+}, "spurious content keys not eliminated after folding");
+
+
+# unless we ask nicely
+
+$xml = q(<opt><x y="one">First</x><x y="two">Second</x></opt>);
+$opt = XMLin(
+  $xml, forcearray => [ 'x' ], keyattr => {x => 'y'}, contentkey => '-content'
+);
+is_deeply($opt, { 
+  x => {
+    one => 'First',
+    two => 'Second',
+  }
+}, "spurious content keys not eliminated after folding");
+
+
+# Check that mixed content parses in the weird way we expect
+
+$xml = q(<opt>
+  <p1 class="mixed">Text with a <b>bold</b> word</p1>
+  <p2>Mixed <b>but</b> no attributes</p2>
+</opt>);
+
+is_deeply(XMLin($xml, @cont_key), {
+  'p1' => {
+    'content' => [ 'Text with a ', ' word' ],
+    'class' => 'mixed',
+    'b' => 'bold'
+  },
+  'p2' => {
+    'content' => [ 'Mixed ', ' no attributes' ],
+    'b' => 'but'
+  }
+}, "mixed content doesn't work - no surprises there");
+
+
+# Confirm single nested element rolls up into a scalar attribute value
+
+$string = q(
+  <opt>
+    <name>value</name>
+  </opt>
+);
+$opt = XMLin($string);
+is_deeply($opt, {
+  name => 'value'
+}, 'nested element rolls up to scalar');
+
+
+# Unless 'forcearray' option is specified
+
+$opt = XMLin($string, forcearray => 1, @cont_key);
+is_deeply($opt, {
+  name => [ 'value' ]
+}, 'except when forcearray is enabled');
+
+
+# Confirm array folding of single nested hash
+
+$string = q(<opt>
+  <inner name="one" value="1" />
+</opt>);
+
+$opt = XMLin($string, forcearray => 1, @cont_key);
+is_deeply($opt, {
+  'inner' => { 'one' => { 'value' => 1 } }
+}, 'array folding works with single nested hash');
+
+
+# But not without forcearray option specified
+
+$opt = XMLin($string, forcearray => 0, @cont_key);
+is_deeply($opt, {
+  'inner' => { 'name' => 'one', 'value' => 1 } 
+}, 'but not if forcearray is turned off');
+
+
+# Test advanced features of forcearray
+
+$xml = q(<opt zero="0">
+  <one>i</one>
+  <two>ii</two>
+  <three>iii</three>
+  <three>3</three>
+  <three>c</three>
+</opt>
+);
+
+$opt = XMLin($xml, forcearray => [ 'two' ], @cont_key);
+is_deeply($opt, {
+  'zero' => '0',
+  'one' => 'i',
+  'two' => [ 'ii' ],
+  'three' => [ 'iii', 3, 'c' ]
+}, 'selective application of forcearray successful');
+
+
+# Test forcearray regexes
+
+$xml = q(<opt zero="0">
+  <one>i</one>
+  <two>ii</two>
+  <three>iii</three>
+  <four>iv</four>
+  <five>v</five>
+</opt>
+);
+
+$opt = XMLin($xml, forcearray => [ qr/^f/, 'two', qr/n/ ], @cont_key);
+is_deeply($opt, {
+  'zero'  => '0',
+  'one'   => [ 'i'  ],
+  'two'   => [ 'ii' ],
+  'three' => 'iii',
+  'four'  => [ 'iv' ],
+  'five'  => [ 'v'  ],
+}, 'forcearray using regex successful');
+
+
+# Same again but a single regexp rather than in an arrayref
+
+$opt = XMLin($xml, forcearray => qr/^f|e$/, @cont_key);
+is_deeply($opt, {
+  'zero'  => '0',
+  'one'   => [ 'i'  ],
+  'two'   =>   'ii',
+  'three' => [ 'iii'],
+  'four'  => [ 'iv' ],
+  'five'  => [ 'v'  ],
+}, 'forcearray using a single regex successful');
+
+
+# Test 'noattr' option
+
+$xml = q(<opt name="user" password="foobar">
+  <nest attr="value">text</nest>
+</opt>
+);
+
+$opt = XMLin($xml, noattr => 1, @cont_key);
+is_deeply($opt, { nest => 'text' }, 'attributes successfully skipped');
+
+
+# And make sure it doesn't screw up array folding 
+
+$xml = q{<opt>
+  <item><key>a</key><value>alpha</value></item>
+  <item><key>b</key><value>beta</value></item>
+  <item><key>g</key><value>gamma</value></item>
+</opt>
+};
+
+
+$opt = XMLin($xml, noattr => 1, @cont_key);
+is_deeply($opt, {
+ 'item' => {
+    'a' => { 'value' => 'alpha' },
+    'b' => { 'value' => 'beta' },
+    'g' => { 'value' => 'gamma' }
+  }
+}, 'noattr does not intefere with array folding');
+
+
+# Confirm empty elements parse to empty hashrefs
+
+$xml = q(<body>
+  <name>bob</name>
+  <outer attr="value">
+    <inner1 />
+    <inner2></inner2>
+  </outer>
+</body>);
+
+$opt = XMLin($xml, noattr => 1, @cont_key);
+is_deeply($opt, {
+  'name' => 'bob',
+  'outer' => {
+    'inner1' => {},
+    'inner2' => {}
+  }
+}, 'empty elements parse to hashrefs');
+
+
+# Unless 'suppressempty' is enabled
+
+$opt = XMLin($xml, noattr => 1, suppressempty => 1, @cont_key);
+is_deeply($opt, { 'name' => 'bob', }, 'or are suppressed');
+
+
+# Check behaviour when 'suppressempty' is set to to undef;
+
+$opt = XMLin($xml, noattr => 1, suppressempty => undef, @cont_key);
+is_deeply($opt, {
+  'name' => 'bob',
+  'outer' => {
+    'inner1' => undef,
+    'inner2' => undef
+  }
+}, "or parse to 'undef'");
+
+# Check behaviour when 'suppressempty' is set to to empty string;
+
+$opt = XMLin($xml, noattr => 1, suppressempty => '', @cont_key);
+is_deeply($opt, {
+  'name' => 'bob',
+  'outer' => {
+    'inner1' => '',
+    'inner2' => ''
+  }
+}, 'or parse to an empty string');
+
+# Confirm completely empty XML parses to undef with 'suppressempty'
+
+$xml = q(<body>
+  <outer attr="value">
+    <inner1 />
+    <inner2></inner2>
+  </outer>
+</body>);
+
+$opt = XMLin($xml, noattr => 1, suppressempty => 1, @cont_key);
+is($opt, undef, 'empty document parses to undef');
+
+
+# Confirm nothing magical happens with grouped elements
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir>/usr/bin</dir>
+    <dir>/usr/local/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml);
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => {
+              dir => [ '/usr/bin', '/usr/local/bin' ]
+            },
+  suffix => 'after',
+}, 'grouped tags parse normally');
+
+
+# unless we specify how the grouping works
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir>/usr/bin</dir>
+    <dir>/usr/local/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml, grouptags => {dirs => 'dir'} );
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => [ '/usr/bin', '/usr/local/bin' ],
+  suffix => 'after',
+}, 'disintermediation of grouped tags works');
+
+
+# try again with multiple groupings
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir>/usr/bin</dir>
+    <dir>/usr/local/bin</dir>
+  </dirs>
+  <infix>between</infix>
+  <terms>
+    <term>vt100</term>
+    <term>xterm</term>
+  </terms>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml, grouptags => {dirs => 'dir', terms => 'term'} );
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => [ '/usr/bin', '/usr/local/bin' ],
+  infix  => 'between',
+  terms  => [ 'vt100', 'xterm' ],
+  suffix => 'after',
+}, 'disintermediation works with multiple groups');
+
+
+# confirm folding and ungrouping work together
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir name="first">/usr/bin</dir>
+    <dir name="second">/usr/local/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml, keyattr => {dir => 'name'}, grouptags => {dirs => 'dir'} );
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => {
+              first  => { content => '/usr/bin' }, 
+              second => { content => '/usr/local/bin' }, 
+            },
+  suffix => 'after',
+}, 'folding and ungrouping work together');
+
+
+# confirm folding, ungrouping and content stripping work together
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir name="first">/usr/bin</dir>
+    <dir name="second">/usr/local/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml,
+  contentkey => '-text', 
+  keyattr    => {dir => 'name'}, 
+  grouptags  => {dirs => 'dir'} 
+);
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => {
+              first  => '/usr/bin', 
+              second => '/usr/local/bin', 
+            },
+  suffix => 'after',
+}, 'folding, ungrouping and content stripping work together');
+
+
+# confirm folding fails as expected even with ungrouping but (no forcearray)
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir name="first">/usr/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml,
+  contentkey => '-text', 
+  keyattr    => {dir => 'name'}, 
+  grouptags  => {dirs => 'dir'} 
+);
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => { name => 'first', text => '/usr/bin'}, 
+  suffix => 'after',
+}, 'folding without forcearray but with ungrouping fails as expected');
+
+
+# but works with forcearray enabled
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <dir name="first">/usr/bin</dir>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml,
+  contentkey => '-text', 
+  forcearray => [ 'dir' ],
+  keyattr    => {dir => 'name'}, 
+  grouptags  => {dirs => 'dir'} 
+);
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => {'first' => '/usr/bin'}, 
+  suffix => 'after',
+}, 'folding with forcearray and ungrouping works');
+
+
+# Test variable expansion - when no variables are defined
+
+$xml = q(<opt>
+  <file name="config_file">${conf_dir}/appname.conf</file>
+  <file name="log_file">${log_dir}/appname.log</file>
+  <file name="debug_file">${log_dir}/appname.dbg</file>
+  <opt docs="${have_docs}" />
+  <bogus value="${undef}" />
+</opt>);
+
+$opt = XMLin($xml, contentkey => '-content');
+is_deeply($opt, {
+  file => {
+    config_file => '${conf_dir}/appname.conf',
+    log_file    => '${log_dir}/appname.log',
+    debug_file  => '${log_dir}/appname.dbg',
+  },
+  opt => { docs => '${have_docs}' },
+  bogus => { value => '${undef}' }
+}, 'undefined variables are left untouched');
+
+
+# try again but with variables defined in advance
+
+$opt = XMLin($xml,
+  contentkey => '-content',
+  variables  => { conf_dir => '/etc', log_dir => '/var/log',
+                  have_docs => 'true' }
+);
+is_deeply($opt, {
+  file => {
+    config_file => '/etc/appname.conf',
+    log_file    => '/var/log/appname.log',
+    debug_file  => '/var/log/appname.dbg',
+  },
+  opt => { docs => 'true' },
+  bogus => { value => '${undef}' }
+}, 'substitution of pre-defined variables works');
+
+
+# now try defining them in the XML
+
+$xml = q(<opt>
+  <dir xsvar="conf_dir">/etc</dir>
+  <dir xsvar="log_dir">/var/log</dir>
+  <cfg xsvar="have_docs">false</cfg>
+  <cfg xsvar="host.domain">search.perl.org</cfg>
+  <cfg xsvar="bad/name">bogus</cfg>
+  <file name="config_file">${conf_dir}/appname.conf</file>
+  <file name="log_file">${log_dir}/appname.log</file>
+  <file name="debug_file">${log_dir}/appname.dbg</file>
+  <file name="bogus_file">${bad/name}</file>
+  <opt docs="${have_docs}" />
+  <site url="http://${host.domain}/" />
+</opt>);
+
+$opt = XMLin($xml, contentkey => '-content', varattr => 'xsvar');
+is_deeply($opt, {
+  file => {
+    config_file => '/etc/appname.conf',
+    log_file    => '/var/log/appname.log',
+    debug_file  => '/var/log/appname.dbg',
+    bogus_file  => '${bad/name}',            # '/' is not valid in a var name
+  },
+  opt           => { docs => 'false' },
+  site          => { url => 'http://search.perl.org/' },
+  dir           => [
+                     { xsvar => 'conf_dir', content => '/etc'     },
+                     { xsvar => 'log_dir',  content => '/var/log' },
+                   ],
+  cfg           => [
+                     { xsvar => 'have_docs',   content => 'false'  },
+                     { xsvar => 'host.domain', content => 'search.perl.org' },
+                     { xsvar => 'bad/name',    content => 'bogus'  },
+                   ],
+}, 'variables defined in XML work');
+
+
+# confirm that variables in XML are merged with pre-defined ones
+
+$xml = q(<opt>
+  <dir xsvar="log_dir">/var/log</dir>
+  <file name="config_file">${conf_dir}/appname.conf</file>
+  <file name="log_file">${log_dir}/appname.log</file>
+  <file name="debug_file">${log_dir}/appname.dbg</file>
+</opt>);
+
+$opt = XMLin($xml,
+  contentkey => '-content', 
+  varattr    => 'xsvar',
+  variables  => { conf_dir => '/etc', log_dir => '/tmp' }
+);
+is_deeply($opt, {
+  file => {
+    config_file => '/etc/appname.conf',
+    log_file    => '/var/log/appname.log',
+    debug_file  => '/var/log/appname.dbg',
+  },
+  dir           => { xsvar => 'log_dir',  content => '/var/log' },
+}, 'variables defined in XML merged successfully with predefined vars');
+
+
+# confirm that a variables are expanded in variable definitions
+
+$xml = q(<opt>
+  <dirs>
+    <dir name="prefix">/usr/local/apache</dir>
+    <dir name="exec_prefix">${prefix}</dir>
+    <dir name="bin_dir">${exec_prefix}/bin</dir>
+  </dirs>
+</opt>);
+
+$opt = XMLin($xml,
+  contentkey => '-content',
+  varattr    => 'name',
+  grouptags  => { dirs => 'dir' },
+);
+is_deeply($opt, {
+  dirs => {
+    prefix      => '/usr/local/apache',
+    exec_prefix => '/usr/local/apache',
+    bin_dir     => '/usr/local/apache/bin',
+  }
+}, 'variables are expanded in later variable definitions');
+
+
+# Confirm only a hash is acceptable to grouptags and variables
+
+$@ = '';
+$_ = eval { $opt = XMLin($xml, grouptags  => [ 'dir' ]); };
+ok(!defined($_), 'grouptags requires a hash');
+like($@, qr/Illegal value for 'GroupTags' option - expected a hashref/, 
+'with correct error message');
+
+$@ = '';
+$_ = eval { $opt = XMLin($xml, variables  => [ 'dir' ]); };
+ok(!defined($_), 'variables requires a hash');
+like($@, qr/Illegal value for 'Variables' option - expected a hashref/, 
+'with correct error message');
+
+
+# Try to disintermediate on the wrong child key
+
+$xml = q(<opt>
+  <prefix>before</prefix>
+  <dirs>
+    <lib>/usr/bin</lib>
+    <lib>/usr/local/bin</lib>
+  </dirs>
+  <suffix>after</suffix>
+</opt>);
+
+$opt = XMLin($xml, grouptags => {dirs => 'dir'} );
+is_deeply($opt, {
+  prefix => 'before',
+  dirs   => { lib => [ '/usr/bin', '/usr/local/bin' ] },
+  suffix => 'after',
+}, 'disintermediation using wrong child key - as expected');
+
+
+# Test option error handling
+
+$@='';
+$_ = eval { XMLin('<x y="z" />', rootname => 'fred') }; # not valid for XMLin()
+is($_, undef, 'invalid options are trapped');
+like($@, qr/Unrecognised option:/, 'with correct error message');
+
+$@='';
+$_ = eval { XMLin('<x y="z" />', 'searchpath') };
+is($_, undef, 'invalid number of options are trapped');
+like($@, qr/Options must be name=>value pairs \(odd number supplied\)/,
+'with correct error message');
+
+
+# Test the NormaliseSpace option
+
+$xml = q(<opt>
+  <user name="  Joe
+  Bloggs  " id="  one  two "/>
+  <user>
+    <name>  Jane
+    Doe </name>
+    <id>
+    three
+    four
+    </id>
+  </user>
+</opt>);
+
+$opt = XMLin($xml, KeyAttr => [ 'name' ], NormaliseSpace => 1);
+ok(ref($opt->{user}) eq 'HASH', "NS-1: folding OK");
+ok(exists($opt->{user}->{'Joe Bloggs'}), "NS-2: space normalised in hash key");
+ok(exists($opt->{user}->{'Jane Doe'}), "NS-3: space normalised in hash key");
+like($opt->{user}->{'Jane Doe'}->{id}, qr{^\s\s+three\s\s+four\s\s+$}s,
+  "NS-4: space not normalised in hash value");
+
+$opt = XMLin($xml, KeyAttr => { user => 'name' }, NormaliseSpace => 1);
+ok(ref($opt->{user}) eq 'HASH', "NS-1a: folding OK");
+ok(exists($opt->{user}->{'Joe Bloggs'}), "NS-2a: space normalised in hash key");
+ok(exists($opt->{user}->{'Jane Doe'}), "NS-3a: space normalised in hash key");
+like($opt->{user}->{'Jane Doe'}->{id}, qr{^\s\s+three\s\s+four\s\s+$}s,
+  "NS-4a: space not normalised in hash value");
+
+$opt = XMLin($xml, KeyAttr => [ 'name' ], NormaliseSpace => 2);
+ok(ref($opt->{user}) eq 'HASH', "NS-5: folding OK");
+ok(exists($opt->{user}->{'Joe Bloggs'}), "NS-6: space normalised in hash key");
+like($opt->{user}->{'Joe Bloggs'}->{id}, qr{^one\stwo$}s,
+  "NS-7: space normalised in attribute value");
+ok(exists($opt->{user}->{'Jane Doe'}), "NS-8: space normalised in hash key");
+like($opt->{user}->{'Jane Doe'}->{id}, qr{^three\sfour$}s,
+  "NS-9: space normalised in element text content");
+
+# confirm NormaliseSpace works in anonymous arrays too
+
+$xml = q(<opt>
+  <anon>  one  two </anon><anon> three
+  four  five </anon><anon> six </anon><anon> seveneightnine </anon>
+</opt>);
+
+$opt = XMLin($xml, NormaliseSpace => 2);
+is_deeply($opt, [   'one two', 'three four five', 'six', 'seveneightnine' ],
+  "NS-10: space normalised in anonymous array");
+
+# Check that American speeling works too
+
+$opt = XMLin($xml, NormalizeSpace => 2);
+is_deeply($opt, [   'one two', 'three four five', 'six', 'seveneightnine' ],
+  "NS-11: space normalized in anonymous array");
+
+# Check that attributes called 'value' are not special
+
+$xml = q(<graphics>
+  <today value="today.png"/>
+  <nav-prev value="prev.png"/>
+  <nav-home value="home.png"/>
+  <nav-next value="next.png"/>
+</graphics>);
+
+$opt = XMLin($xml);
+
+is_deeply($opt, {
+  'today'    => { value => "today.png" },
+  'nav-prev' => { value => "prev.png"  },
+  'nav-home' => { value => "home.png"  },
+  'nav-next' => { value => "next.png"  },
+}, "Nothing special about 'value' attributes");
+
+# Now turn on the ValueAttr option and try again
+
+$opt = XMLin($xml, ValueAttr => [ 'value' ]);
+
+is_deeply($opt, {
+  'today'    => "today.png",
+  'nav-prev' => "prev.png",
+  'nav-home' => "home.png",
+  'nav-next' => "next.png",
+}, "ValueAttr as arrayref works");
+
+# Try with a list of different ValueAttr names
+
+$xml = q(<graphics>
+  <today xxx="today.png"/>
+  <nav-prev yyy="prev.png"/>
+  <nav-home zzz="home.png"/>
+  <nav-next value="next.png"/>
+</graphics>);
+
+$opt = XMLin($xml, ValueAttr => [ qw(xxx yyy zzz) ]);
+
+is_deeply($opt, {
+  'today'    => "today.png",
+  'nav-prev' => "prev.png",
+  'nav-home' => "home.png",
+  'nav-next' => { value => "next.png" },
+}, "ValueAttr as arrayref works");
+
+# Try specifying ValueAttr as a hashref
+
+$xml = q(<graphics>
+  <today xxx="today.png"/>
+  <nav-prev value="prev.png"/>
+  <nav-home yyy="home.png"/>
+  <nav-next value="next.png"/>
+</graphics>);
+
+$opt = XMLin($xml, 
+  ValueAttr => {
+    'today'    => 'xxx', 
+    'nav-home' => 'yyy', 
+    'nav-next' => 'value'
+  }
+);
+
+is_deeply($opt, {
+  'today'    => "today.png",
+  'nav-prev' => { value => "prev.png" },
+  'nav-home' => "home.png",
+  'nav-next' => "next.png",
+}, "ValueAttr as hashref works too");
+
+# Confirm that there's no conflict with KeyAttr or ContentKey defaults
+
+$xml = q(<graphics>
+  <today value="today.png"/>
+  <animal name="lion" age="7"/>
+  <animal name="elephant" age="97"/>
+  <colour rgb="#FF0000">red</colour>
+</graphics>);
+
+$opt = XMLin($xml, ValueAttr => { 'today'    => 'value' });
+
+is_deeply($opt, {
+  today  => 'today.png',
+  animal => {
+    lion     => { age =>  7 },
+    elephant => { age => 97 },
+  },
+  colour => { rgb => '#FF0000', content => 'red' },
+}, "ValueAttr as hashref works too");
+
+# Now for a 'real world' test, try slurping in an SRT config file
+
+$opt = XMLin(File::Spec->catfile('t', 'srt.xml'),
+  forcearray => 1, @cont_key
+);
+$target = {
+  'global' => [
+    {
+      'proxypswd' => 'bar',
+      'proxyuser' => 'foo',
+      'exclude' => [
+        '/_vt',
+        '/save\\b',
+        '\\.bak$',
+        '\\.\\$\\$\\$$'
+      ],
+      'httpproxy' => 'http://10.1.1.5:8080/',
+      'tempdir' => 'C:/Temp'
+    }
+  ],
+  'pubpath' => {
+    'test1' => {
+      'source' => [
+        {
+          'label' => 'web_source',
+          'root' => 'C:/webshare/web_source'
+        }
+      ],
+      'title' => 'web_source -> web_target1',
+      'package' => {
+        'images' => { 'dir' => 'wwwroot/images' }
+      },
+      'target' => [
+        {
+          'label' => 'web_target1',
+          'root' => 'C:/webshare/web_target1',
+          'temp' => 'C:/webshare/web_target1/temp'
+        }
+      ],
+      'dir' => [ 'wwwroot' ]
+    },
+    'test2' => {
+      'source' => [
+        {
+          'label' => 'web_source',
+          'root' => 'C:/webshare/web_source'
+        }
+      ],
+      'title' => 'web_source -> web_target1 & web_target2',
+      'package' => {
+        'bios' => { 'dir' => 'wwwroot/staff/bios' },
+        'images' => { 'dir' => 'wwwroot/images' },
+        'templates' => { 'dir' => 'wwwroot/templates' }
+      },
+      'target' => [
+        {
+          'label' => 'web_target1',
+          'root' => 'C:/webshare/web_target1',
+          'temp' => 'C:/webshare/web_target1/temp'
+        },
+        {
+          'label' => 'web_target2',
+          'root' => 'C:/webshare/web_target2',
+          'temp' => 'C:/webshare/web_target2/temp'
+        }
+      ],
+      'dir' => [ 'wwwroot' ]
+    },
+    'test3' => {
+      'source' => [
+        {
+          'label' => 'web_source',
+          'root' => 'C:/webshare/web_source'
+        }
+      ],
+      'title' => 'web_source -> web_target1 via HTTP',
+      'addexclude' => [ '\\.pdf$' ],
+      'target' => [
+        {
+          'label' => 'web_target1',
+          'root' => 'http://127.0.0.1/cgi-bin/srt_slave.plx',
+          'noproxy' => 1
+        }
+      ],
+      'dir' => [ 'wwwroot' ]
+    }
+  }
+};
+is_deeply($opt, $target, 'successfully read an SRT config file');
+
+
+exit(0);
+
+
+sub warn_handler {
+  $last_warning = $_[0];
+}
diff --git a/t/1_XMLin.xml b/t/1_XMLin.xml
new file mode 100644 (file)
index 0000000..637b39a
--- /dev/null
@@ -0,0 +1 @@
+<opt location="t/1_XMLin.xml" />
diff --git a/t/2_XMLout.t b/t/2_XMLout.t
new file mode 100644 (file)
index 0000000..6d7fdf9
--- /dev/null
@@ -0,0 +1,1214 @@
+# $Id: 2_XMLout.t,v 1.17 2006/10/05 08:28:05 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+
+$^W = 1;
+
+plan tests => 201;
+
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Read file and return contents as a scalar.
+#
+
+sub ReadFile {
+  local($/) = undef;
+
+  open(_READ_FILE_, $_[0]) || die "open($_[0]): $!";
+  my $data = <_READ_FILE_>;
+  close(_READ_FILE_);
+  return($data);
+}
+
+use XML::Simple;
+
+# Confirm error when mandatory parameter missing
+
+$_ = eval {
+  XMLout();
+};
+ok(!defined($_), 'call with no args proves fatal');
+like($@, qr/XMLout\(\) requires at least one argument/, 
+'with correct error message');
+
+# Try encoding a scalar value
+
+my $xml = XMLout("scalar");
+ok(1, 'XMLout did not crash');
+ok(defined($xml), 'and it returned an XML string');
+is(XMLin($xml), 'scalar', 'which parses back OK');
+
+
+# Next try encoding a hash
+
+my $hashref1 = { one => 1, two => 'II', three => '...' };
+my $hashref2 = { one => 1, two => 'II', three => '...' };
+
+# Expect:
+# <opt one="1" two="II" three="..." />
+
+$_ = XMLout($hashref1);
+is_deeply(XMLin($_), $hashref1, 'encoded a hash');
+ok(s/one="1"//, 'first key encoded OK');
+ok(s/two="II"//, 'second key encoded OK');
+ok(s/three="..."//, 'third key encoded OK');
+like($_, qr/^<\w+\s+\/>/, 'no other attributes encoded');
+
+
+# Now try encoding a hash with a nested array
+
+my $ref = {array => [qw(one two three)]};
+# Expect:
+# <opt>
+#   <array>one</array>
+#   <array>two</array>
+#   <array>three</array>
+# </opt>
+
+$_ = XMLout($ref);
+is_deeply(XMLin($_), $ref, 'encoded a hash with nested array');
+ok(s{<array>one</array>\s*
+         <array>two</array>\s*
+         <array>three</array>}{}sx, 'array elements encoded in correct order');
+like($_, qr/^<(\w+)\s*>\s*<\/\1>\s*$/s, 'no other spurious encodings');
+
+
+# Now try encoding a nested hash
+
+$ref = { value => '555 1234',
+         hash1 => { one => 1 },
+         hash2 => { two => 2 } };
+# Expect:
+# <opt value="555 1234">
+#   <hash1 one="1" />
+#   <hash2 two="2" />
+# </opt>
+
+$_ = XMLout($ref);
+is_deeply(XMLin($_), $ref, 'encoded nested hashes');
+
+ok(s{<hash1 one="1" />\s*}{}s, 'nested hash 1 ok');
+ok(s{<hash2 two="2" />\s*}{}s, 'nested hash 2 ok');
+like($_, qr{^<(\w+)\s+value="555 1234"\s*>\s*</\1>\s*$}s, 'whole OK');
+
+
+# Now try encoding an anonymous array
+
+$ref = [ qw(1 two III) ];
+# Expect:
+# <opt>
+#   <anon>1</anon>
+#   <anon>two</anon>
+#   <anon>III</anon>
+# </opt>
+
+$_ = XMLout($ref);
+is_deeply(XMLin($_), $ref, 'encoded anonymous array');
+
+like($_, qr{
+  ^<(\w+)\s*>
+  \s*<anon>1</anon>
+  \s*<anon>two</anon>
+  \s*<anon>III</anon>
+  \s*</\1>\s*$}sx, 'output matches expectations');
+
+
+# Now try encoding a nested anonymous array
+
+$ref = [ [ qw(1.1 1.2) ], [ qw(2.1 2.2) ] ];
+# Expect:
+# <opt>
+#   <anon>
+#     <anon>1.1</anon>
+#     <anon>1.2</anon>
+#   </anon>
+#   <anon>
+#     <anon>2.1</anon>
+#     <anon>2.2</anon>
+#   </anon>
+# </opt>
+
+$_ = XMLout($ref);
+is_deeply(XMLin($_), $ref, 'encoded nested anonymous arrays');
+
+like($_, qr{
+  <(\w+)\s*>
+  \s*<anon\s*>
+  \s*<anon\s*>1\.1</anon\s*>
+  \s*<anon\s*>1\.2</anon\s*>
+  \s*</anon\s*>
+  \s*<anon\s*>
+  \s*<anon\s*>2\.1</anon\s*>
+  \s*<anon\s*>2\.2</anon\s*>
+  \s*</anon\s*>
+  \s*</\1\s*>
+}sx, 'output matches expectations');
+
+
+# Now try encoding a hash of hashes with key folding disabled
+
+$ref = { country => {
+                     England => { capital => 'London' },
+                     France  => { capital => 'Paris' },
+                     Turkey  => { capital => 'Istanbul' },
+                    }
+       };
+# Expect:
+# <opt>
+#   <country>
+#     <England capital="London" />
+#     <France capital="Paris" />
+#     <Turkey capital="Istanbul" />
+#   </country>
+# </opt>
+
+$_ = XMLout($ref, keyattr => []);
+is_deeply(XMLin($_), $ref, 'encoded hash of hashes with folding disabled');
+ok(s{<England\s+capital="London"\s*/>\s*}{}s, 'nested hash 1 ok');
+ok(s{<France\s+capital="Paris"\s*/>\s*}{}s, 'nested hash 2 ok');
+ok(s{<Turkey\s+capital="Istanbul"\s*/>\s*}{}s, 'nested hash 3 ok');
+ok(s{<country\s*>\s*</country>}{}s, 'container hash ok');
+ok(s{^<(\w+)\s*>\s*</\1>$}{}s, 'document ok');
+
+
+# Try encoding same again with key folding set to non-standard value
+
+# Expect:
+# <opt>
+#   <country fullname="England" capital="London" />
+#   <country fullname="France" capital="Paris" />
+#   <country fullname="Turkey" capital="Istanbul" />
+# </opt>
+
+my $expected = qr{
+  ^<(\w+)\s*>\s*
+    (
+      <country(\s*fullname="Turkey"|\s*capital="Istanbul"){2}\s*/>\s*
+     |<country(\s*fullname="France"|\s*capital="Paris"){2}\s*/>\s*
+     |<country(\s*fullname="England"|\s*capital="London"){2}\s*/>\s*
+    ){3}
+  </\1>$
+}xs;
+
+$xml = XMLout($ref, keyattr => ['fullname']);
+is_deeply(XMLin($xml, keyattr => ['fullname']), $ref,
+'encoded hash of hashes with explicit folding enabled');
+
+like($xml, $expected, 'output as expected');
+
+
+# Same again but specify name as scalar rather than array
+
+$xml = XMLout($ref, keyattr => 'fullname');
+like($xml, $expected, 'still works when keyattr is scalar');
+
+
+# Same again but specify keyattr as hash rather than array
+
+$xml = XMLout($ref, keyattr => { country => 'fullname' });
+like($xml, $expected, 'still works when keyattr is hash');
+
+
+# Same again but add leading '+'
+
+$xml = XMLout($ref, keyattr => { country => '+fullname' });
+like($xml, $expected, "still works when keyattr is hash with leading '+'");
+
+
+# and leading '-'
+
+$xml = XMLout($ref, keyattr => { country => '-fullname' });
+like($xml, $expected, "still works when keyattr is hash with leading '-'");
+
+
+# One more time but with default key folding values
+
+# Expect:
+# <opt>
+#   <country name="England" capital="London" />
+#   <country name="France" capital="Paris" />
+#   <country name="Turkey" capital="Istanbul" />
+# </opt>
+
+$expected = qr{
+  ^<(\w+)\s*>\s*
+    (
+      <country(\s*name="Turkey"|\s*capital="Istanbul"){2}\s*/>\s*
+     |<country(\s*name="France"|\s*capital="Paris"){2}\s*/>\s*
+     |<country(\s*name="England"|\s*capital="London"){2}\s*/>\s*
+    ){3}
+  </\1>$
+}xs;
+
+$xml = XMLout($ref);
+is_deeply(XMLin($xml), $ref,
+'encoded hash of hashes with default folding enabled');
+like($xml, $expected, "expected output with default keyattr");
+
+
+# Finally, confirm folding still works with only one nested hash
+
+# Expect:
+# <opt>
+#   <country name="England" capital="London" />
+# </opt>
+
+$ref = { country => { England => { capital => 'London' } } };
+$_ = XMLout($ref);
+is_deeply(XMLin($_, forcearray => 1), $ref, 'single nested hash unfolded');
+ok(s{\s*name="England"}{uk}s, 'attr 1 ok');
+ok(s{\s*capital="London"}{uk}s, 'attr 2 ok');
+ok(s{<countryukuk\s*/>\s*}{}s, 'element ok');
+ok(s{^<(\w+)\s*>\s*</\1>$}{}s, 'document ok');
+
+
+# Check that default XML declaration works
+#
+# Expect:
+# <?xml version='1.0' standalone='yes'?>
+# <opt one="1" />
+
+$ref = { one => 1 };
+
+$_ = XMLout($ref, xmldecl => 1);
+is_deeply(XMLin($_), $ref, 'generated doc with XML declaration');
+ok(s{^\Q<?xml version='1.0' standalone='yes'?>\E}{}s, 'XML declaration OK');
+like($_, qr{^\s*<opt\s+one="1"\s*/>}s, 'data OK too');
+
+
+# Check that custom XML declaration works
+#
+# Expect:
+# <?xml version='1.0' standalone='yes'?>
+# <opt one="1" />
+
+$_ = XMLout($ref, xmldecl => "<?xml version='1.0' standalone='yes'?>");
+is_deeply(XMLin($_), $ref, 'generated doc with custom XML declaration');
+ok(s{^\Q<?xml version='1.0' standalone='yes'?>\E}{}s, 'XML declaration OK');
+like($_, qr{^\s*<opt\s+one="1"\s*/>}s, 'data OK too');
+
+
+# Check that special characters do get escaped
+
+$ref = { a => '<A>', b => '"B"', c => '&C&' };
+$_ = XMLout($ref);
+is_deeply(XMLin($_), $ref, 'generated document with escaping');
+ok(s{a="&lt;A&gt;"}{}s, 'angle brackets escaped OK');
+ok(s{b="&quot;B&quot;"}{}s, 'double quotes escaped OK');
+ok(s{c="&amp;C&amp;"}{}s, 'ampersands escaped OK');
+ok(s{^<(\w+)\s*/>$}{}s, 'data OK too');
+
+
+# unless we turn escaping off
+
+$ref = { a => '<A>', b => '"B"', c => ['&C&'] };
+$_ = XMLout($ref, noescape => 1);
+ok(s{a="<A>"}{}s, 'generated unescaped angle brackets');
+ok(s{b=""B""}{}s, 'generated unescaped double quotes');
+ok(s{<c>&C&</c>}{}s, 'generated unescaped ampersands');
+ok(s{^<(\w+)\s*>\s*</\1>$}{}s, 'data OK too');
+
+# same again but with a scalar
+
+$xml = XMLout("<scalar>", noescape => 1);
+like($xml, qr{^<(\w+)><scalar></\1>}, "Unescaped scalar as expected too");
+
+# Try encoding a circular data structure and confirm that it fails
+
+$_ = eval {
+  my $ref = { a => '1' };
+  $ref->{b} = $ref;
+  XMLout($ref);
+};
+ok(!defined($_), 'caught circular data structure');
+like($@, qr/circular data structures not supported/, 
+'with correct error message');
+
+
+# Try encoding a repetitive (but non-circular) data structure and confirm that 
+# it does not fail
+
+$_ = eval {
+  my $a = { alpha => 1 };
+  my $ref = { a => $a, b => $a };
+  XMLout($ref);
+};
+ok(defined($_), 'repetitive (non-circular) data structure not fatal');
+like($_, qr{^
+<opt>
+  (
+    \s*<a\s+alpha="1"\s*/>
+  |
+    \s*<b\s+alpha="1"\s*/>
+  ){2}
+\s*</opt>
+}xs, 'and encodes as expected');
+
+
+# Try encoding a non array/hash blessed reference and confirm that it fails
+
+$_ = eval { my $ref = bless \*STDERR, 'BogoClass'; XMLout($ref) };
+is($_, undef, 'caught blessed non array/hash reference in data structure');
+like($@, qr/Can't encode a value of type: /, 'with correct error message');
+
+
+# Repeat some of the above tests with named root element
+
+# Try encoding a scalar value
+
+$xml = XMLout("scalar", rootname => 'TOM');
+ok(defined($xml), 'generated document with named root element');
+is(XMLin($xml), 'scalar', 'parsed it back correctly');
+like($xml, qr/^\s*<TOM>scalar<\/TOM>\s*$/si, 'XML as expected');
+
+
+# Next try encoding a hash
+
+# Expect:
+# <DICK one="1" two="II" three="..." />
+
+$_ = XMLout($hashref1, rootname => 'DICK');
+is_deeply(XMLin($_), $hashref1, 'same again but encoded a hash');
+ok(s/one="1"//, 'first key encoded OK');
+ok(s/two="II"//, 'second key encoded OK');
+ok(s/three="..."//, 'third key encoded OK');
+like($_, qr/^<DICK\s+\/>/, 'XML looks OK');
+
+
+# Now try encoding a hash with a nested array
+
+$ref = {array => [qw(one two three)]};
+# Expect:
+# <LARRY>
+#   <array>one</array>
+#   <array>two</array>
+#   <array>three</array>
+# </LARRY>
+
+$_ = XMLout($ref, rootname => 'LARRY');
+is_deeply(XMLin($_), $ref, 'same again but with array in hash');
+ok(s{<array>one</array>\s*
+         <array>two</array>\s*
+         <array>three</array>}{}sx,  'array encoded in correct order');
+like($_, qr/^<(LARRY)\s*>\s*<\/\1>\s*$/s, 'only expected root element left');
+
+
+# Now try encoding a nested hash
+
+$ref = { value => '555 1234',
+         hash1 => { one => 1 },
+         hash2 => { two => 2 } };
+# Expect:
+# <CURLY value="555 1234">
+#   <hash1 one="1" />
+#   <hash2 two="2" />
+# </CURLY>
+
+$_ = XMLout($ref, rootname => 'CURLY');
+is_deeply(XMLin($_), $ref, 'same again but with nested hashes');
+
+ok(s{<hash1 one="1" />\s*}{}s, 'hash 1 encoded OK');
+ok(s{<hash2 two="2" />\s*}{}s, 'hash 2 encoded OK');
+like($_, qr{^<(CURLY)\s+value="555 1234"\s*>\s*</\1>\s*$}s, 'document OK');
+
+
+# Now try encoding an anonymous array
+
+$ref = [ qw(1 two III) ];
+# Expect:
+# <MOE>
+#   <anon>1</anon>
+#   <anon>two</anon>
+#   <anon>III</anon>
+# </MOE>
+
+$_ = XMLout($ref, rootname => 'MOE');
+is_deeply(XMLin($_), $ref, 'same again but with nested anonymous array');
+like($_, qr{
+  ^<(MOE)\s*>
+    \s*<anon>1</anon>
+    \s*<anon>two</anon>
+    \s*<anon>III</anon>
+  \s*</\1>\s*$}sx, 'document OK');
+
+
+# Test again, this time with no root element
+
+# Try encoding a scalar value
+
+like(XMLout("scalar", rootname => ''),    qr/scalar\s+/s, 
+  'encoded scalar with no root element');
+like(XMLout("scalar", rootname => undef), qr/scalar\s+/s, 
+  'same again but with rootname = undef');
+
+
+# Next try encoding a hash
+
+# Expect:
+#   <one>1</one>
+#   <two>II</two>
+#   <three>...</three>
+
+$_ = XMLout($hashref1, rootname => '');
+is_deeply(XMLin("<opt>$_</opt>"), $hashref1,
+  'generated doc with no root element from hash');
+ok(s/<one>1<\/one>//, 'first key encoded OK');
+ok(s/<two>II<\/two>//, 'second key encoded OK');
+ok(s/<three>...<\/three>//, 'third key encoded OK');
+like($_, qr/^\s*$/, 'document OK');
+
+
+# Now try encoding a nested hash
+
+$ref = { value => '555 1234',
+         hash1 => { one => 1 },
+         hash2 => { two => 2 } };
+# Expect:
+#   <value>555 1234</value>
+#   <hash1 one="1" />
+#   <hash2 two="2" />
+
+$_ = XMLout($ref, rootname => '');
+is_deeply(XMLin("<opt>$_</opt>"), $ref,
+  'generated docucment with no root element from nested hashes');
+ok(s{<value>555 1234<\/value>\s*}{}s, 'first element OK');
+ok(s{<hash1 one="1" />\s*}{}s, 'second element OK');
+ok(s{<hash2 two="2" />\s*}{}s, 'third element OK');
+like($_, qr{^\s*$}s, 'document OK');
+
+
+# Now try encoding an anonymous array
+
+$ref = [ qw(1 two III) ];
+# Expect:
+#   <anon>1</anon>
+#   <anon>two</anon>
+#   <anon>III</anon>
+
+$_ = XMLout($ref, rootname => '');
+is_deeply(XMLin("<opt>$_</opt>"), $ref,
+  'generated doc with no root name from array');
+like($_, qr{
+  ^\s*<anon>1</anon>
+  \s*<anon>two</anon>
+  \s*<anon>III</anon>
+  \s*$}sx, 'document OK');
+
+
+# Test option error handling
+
+$_ = eval { XMLout($hashref1, searchpath => []) }; # only valid for XMLin()
+ok(!defined($_), 'caught attempt to specify searchpath on XMLout');
+like($@, qr/Unrecognised option:/, 'with correct error message');
+
+$_ = eval { XMLout($hashref1, 'bogus') };
+ok(!defined($_), 'caught attempt to specify odd number of option args');
+like($@, qr/Options must be name=>value pairs \(odd number supplied\)/,
+  'with correct error message');
+
+
+# Test output to file
+
+my $TestFile = 'testoutput.xml';
+unlink($TestFile);
+ok(!-e $TestFile, 'output file does not exist');
+
+$xml = XMLout($hashref1);
+eval { XMLout($hashref1, outputfile => $TestFile); };
+ok(-e $TestFile, 'created xml output file');
+is(ReadFile($TestFile), $xml, 'Contents match expectations');
+unlink($TestFile);
+
+
+# Test output to an IO handle
+
+ok(!-e $TestFile);
+eval {
+  open my $fh, '>', $TestFile or die "$!";
+  XMLout($hashref1, outputfile => $fh);
+  $fh->close();
+};
+ok(-e $TestFile, 'create XML output file via IO::File');
+is(ReadFile($TestFile), $xml, 'Contents match expectations');
+unlink($TestFile);
+
+# After all that, confirm that the original hashref we supplied has not
+# been corrupted.
+
+is_deeply($hashref1, $hashref2, 'original data not corrupted');
+
+
+# Confirm that hash keys with leading '-' are skipped
+
+$ref = {
+  'a'  => 'one',
+  '-b' => 'two',
+  '-c' => {
+           'one' => 1,
+           'two' => 2
+          }
+};
+
+$_ = XMLout($ref, rootname => 'opt');
+like($_, qr{^\s*<opt\s+a="one"\s*/>\s*$}s, "skipped hashkeys with '-' prefix");
+
+
+# Try a more complex unfolding with key attributes named in a hash
+
+$ref = {
+  'car' => {
+    'LW1804' => {
+      'option' => {
+        '9926543-1167' => { 'key' => 1, 'desc' => 'Steering Wheel' }
+      },
+      'id' => 2,
+      'make' => 'GM'
+    },
+    'SH6673' => {
+      'option' => {
+        '6389733317-12' => { 'key' => 2, 'desc' => 'Electric Windows' },
+        '3735498158-01' => { 'key' => 3, 'desc' => 'Leather Seats' },
+        '5776155953-25' => { 'key' => 4, 'desc' => 'Sun Roof' },
+      },
+      'id' => 1,
+      'make' => 'Ford'
+    }
+  }
+};
+
+# Expect:
+# <opt>
+#   <car license="LW1804" id="2" make="GM">
+#     <option key="1" pn="9926543-1167" desc="Steering Wheel" />
+#   </car>
+#   <car license="SH6673" id="1" make="Ford">
+#     <option key="2" pn="6389733317-12" desc="Electric Windows" />
+#     <option key="3" pn="3735498158-01" desc="Leather Seats" />
+#     <option key="4" pn="5776155953-25" desc="Sun Roof" />
+#   </car>
+# </opt>
+
+$_ = XMLout($ref, keyattr => { 'car' => 'license', 'option' => 'pn' });
+is_deeply(XMLin($_,
+  forcearray => 1,
+  keyattr => { 'car' => 'license', 'option' => 'pn' }
+), $ref, 'generated document from complex nested hash with unfolding');
+ok(s{\s*make="GM"}{gm}s, 'element 1 attribute 1 OK');
+ok(s{\s*id="2"}{gm}s, 'element 1 attribute 2 OK');
+ok(s{\s*license="LW1804"}{gm}s, 'element 1 attribute 3 OK');
+ok(s{\s*desc="Steering Wheel"}{opt}s, 'element 1.1 attribute 1 OK');
+ok(s{\s*pn="9926543-1167"}{opt}s, 'element 1.1 attribute 2 OK');
+ok(s{\s*key="1"}{opt}s, 'element 1.1 attribute 3 OK');
+ok(s{\s*<cargmgmgm>\s*<optionoptoptopt\s*/>\s*</car>}{CAR}s,
+  'elements 1 and 1.1 OK');
+ok(s{\s*make="Ford"}{ford}s, 'element 2 attribute 1 OK');
+ok(s{\s*id="1"}{ford}s, 'element 2 attribute 2 OK');
+ok(s{\s*license="SH6673"}{ford}s, 'element 2 attribute 3 OK');
+ok(s{\s*desc="Electric Windows"}{1}s, 'element 2.1 attribute 1 OK');
+ok(s{\s*pn="6389733317-12"}{1}s, 'element 2.1 attribute 2 OK');
+ok(s{\s*key="2"}{1}s, 'element 2.1 attribute 3 OK');
+ok(s{\s*<option111}{<option}s, 'element 2.1 OK');
+ok(s{\s*desc="Leather Seats"}{2}s, 'element 2.2 attribute 1 OK');
+ok(s{\s*pn="3735498158-01"}{2}s, 'element 2.2 attribute 2 OK');
+ok(s{\s*key="3"}{2}s, 'element 2.2 attribute 3 OK');
+ok(s{\s*<option222}{<option}s, 'element 2.2 OK');
+ok(s{\s*desc="Sun Roof"}{3}s, 'element 2.3 attribute 1 OK');
+ok(s{\s*pn="5776155953-25"}{3}s, 'element 2.3 attribute 2 OK');
+ok(s{\s*key="4"}{3}s, 'element 2.3 attribute 3 OK');
+ok(s{\s*<option333}{<option}s, 'element 2.3 OK');
+ok(s{\s*<carfordfordford>\s*(<option\s*/>\s*){3}</car>}{CAR}s, 'element 2 OK');
+ok(s{^<(\w+)\s*>\s*CAR\s*CAR\s*</\1>$}{}s, 'document OK');
+
+
+# Check that empty hashes translate to empty tags
+
+$ref = {
+  'one' => {
+    'attr1' => 'avalue1',
+    'nest1' => [ 'nvalue1' ],
+    'nest2' => {}
+  },
+  two => {}
+};
+
+$_ = XMLout($ref);
+ok(s{<nest2\s*></nest2\s*>\s*}{<NNN>}, 'nested empty hash OK');
+ok(s{<nest1\s*>nvalue1</nest1\s*>\s*}{<NNN>}, 'array OK');
+ok(s{<one\s*attr1\s*=\s*"avalue1">\s*}{<one>}, 'scalar OK');
+ok(s{<one\s*>\s*<NNN>\s*<NNN>\s*</one>}{<nnn>}, 'nesting OK');
+ok(s{<two\s*></two\s*>\s*}{<nnn>}, 'empty hash OK');
+like($_, qr{^\s*<(\w+)\s*>\s*<nnn>\s*<nnn>\s*</\1\s*>\s*$}, 'document OK');
+
+
+# Check undefined values generate warnings 
+
+{
+  local($^W) = 1;
+  my $warn = '';
+  local $SIG{__WARN__} = sub { $warn = $_[0] };
+  $ref = { 'one' => 1, 'two' => undef };
+  my $expect = qr/^<\w+(\s+one="1"|\s+two=""){2}/;
+
+  $_ = XMLout($ref);
+  like($warn, qr/Use of uninitialized value/, 
+    'caught warning re uninitialised value');
+  like($_, $expect, 'undef maps to any empty attribute by default');
+
+  # unless warnings are disabled
+  $^W = 0;
+  $warn = '';
+  $_ = XMLout($ref);
+  is($warn, '', 'no warning re uninitialised value if warnings off');
+  like($_, $expect, 'undef still maps to any empty attribute');
+}
+
+
+# Unless undef is mapped to empty elements
+
+$ref = { 'tag' => undef };
+$_ = XMLout($ref, suppressempty => undef);
+like($_, qr{^\s*<(\w*)\s*>\s*<tag\s*></tag\s*>\s*</\1\s*>\s*$}s,
+  'uninitialiased values successfully mapped to empty elements');
+
+
+# Set suppressempty to 1 to not output anything for undef
+
+$ref = { 'one' => 1, 'two' => undef };
+$_ = XMLout($ref, suppressempty => 1, noattr => 1);
+like($_, qr{^\s*<(\w*)\s*>\s*<one\s*>1</one\s*>\s*</\1\s*>\s*$}s,
+  'uninitialiased values successfully skipped');
+
+
+# Try undef in an array
+
+$ref = { a => [ 'one', undef, 'three' ] };
+$_ = XMLout($ref);
+like($_, 
+  qr{
+    ^\s*<(\w*)\s*>
+    \s*<a\s*>one</a\s*>
+    \s*<a\s*></a\s*>
+    \s*<a\s*>three</a\s*>
+    \s*</\1\s*>\s*$
+  }xs,
+  'uninitialiased value in array is empty element');
+
+
+# And again with SuppressEmpty enabled
+
+$_ = XMLout($ref, SuppressEmpty => 1);
+like($_, 
+  qr{
+    ^\s*<(\w*)\s*>
+    \s*<a\s*>one</a\s*>
+    \s*<a\s*>three</a\s*>
+    \s*</\1\s*>\s*$
+  }xs,
+  'uninitialiased value in array is skipped');
+
+
+# Test the keeproot option
+
+$ref = {
+  'seq' => {
+    'name' => 'alpha',
+    'alpha' => [ 1, 2, 3 ]
+  }
+};
+
+my $xml1 = XMLout($ref, rootname => 'sequence');
+my $xml2 = XMLout({ 'sequence' => $ref }, keeproot => 1);
+
+is_deeply($xml1, $xml2, 'keeproot works as expected');
+
+
+# Test that items with text content are output correctly
+# Expect: <opt one="1">text</opt>
+
+$ref = { 'one' => 1, 'content' => 'text' };
+
+$_ = XMLout($ref);
+
+like($_, qr{^\s*<opt\s+one="1">text</opt>\s*$}s, 'content keys mapped OK');
+
+
+# Even if we change the default value for the 'contentkey' option
+
+$ref = { 'one' => 1, 'text_content' => 'text' };
+
+$_ = XMLout($ref, contentkey => 'text_content');
+
+like($_, qr{^\s*<opt\s+one="1">text</opt>\s*$}s, 'even when name changed');
+
+
+# and also if we add the '-' prefix
+
+$_ = XMLout($ref, contentkey => '-text_content');
+
+like($_, qr{^\s*<opt\s+one="1">text</opt>\s*$}s, 'even with "-" prefix');
+
+
+# Confirm content key works with undef values (and no warnings)
+
+{
+  $^W = 1;
+  my $warn = '';
+  local $SIG{__WARN__} = sub { $warn = $_[0] };
+  $_ = eval {
+    $ref = { 
+      column => [
+        { name => 'title',   content => 'A Title' },
+        { name => 'sponsor', content => undef },
+      ],
+    };
+    XMLout($ref, suppress_empty => undef, content_key => 'content');
+  };
+  ok(!$warn,  'no warnings with suppress_empty => undef');
+  like($_, qr{^<(\w+)>
+      \s*<column\s+name="title"\s*>A\sTitle</column>
+      \s*<column\s+name="sponsor"\s*></column>
+      \s*
+      </\1>$
+    }sx, "undef does not cause content tags in output"
+  );
+}
+
+
+# Check 'noattr' option
+
+$ref = {
+  attr1  => 'value1',
+  attr2  => 'value2',
+  nest   => [ qw(one two three) ]
+};
+
+# Expect:
+#
+# <opt>
+#   <attr1>value1</attr1>
+#   <attr2>value2</attr2>
+#   <nest>one</nest>
+#   <nest>two</nest>
+#   <nest>three</nest>
+# </opt>
+#
+
+$_ = XMLout($ref, noattr => 1);
+
+unlike($_, qr{=}s, 'generated document with no attributes');
+is_deeply(XMLin($_), $ref, 'parses ok');
+ok(s{\s*<(attr1)>value1</\1>\s*}{NEST}s, 'scalar 1 mapped ok');
+ok(s{\s*<(attr2)>value2</\1>\s*}{NEST}s, 'scalar 2 mapped ok');
+ok(s{\s*<(nest)>one</\1>\s*<\1>two</\1>\s*<\1>three</\1>}{NEST}s, 
+'array mapped ok');
+like($_, qr{^<(\w+)\s*>(NEST\s*){3}</\1>$}s, 'document OK');
+
+
+# Check noattr doesn't screw up keyattr
+
+$ref = { number => {
+  'twenty one' => { dec => 21, hex => '0x15' },
+  'thirty two' => { dec => 32, hex => '0x20' }
+  }
+};
+
+# Expect:
+#
+# <opt>
+#   <number>
+#     <dec>21</dec>
+#     <word>twenty one</word>
+#     <hex>0x15</hex>
+#   </number>
+#   <number>
+#     <dec>32</dec>
+#     <word>thirty two</word>
+#     <hex>0x20</hex>
+#   </number>
+# </opt>
+#
+
+$_ = XMLout($ref, noattr => 1, keyattr => [ 'word' ]);
+
+unlike($_, qr{=}s, 'same again but with unfolding too');
+is_deeply(XMLin($_, keyattr => [ 'word' ]), $ref, 'parsed OK');
+ok(s{\s*<(dec)>21</\1>\s*}{21}s, 'scalar 1.1 mapped OK');
+ok(s{\s*<(hex)>0x15</\1>\s*}{21}s, 'scalar 1.2 mapped OK');
+ok(s{\s*<(word)>twenty one</\1>\s*}{21}s, 'scalar 1.3 mapped OK');
+ok(s{\s*<(number)>212121</\1>\s*}{NUM}s, 'element 1 OK');
+ok(s{\s*<(dec)>32</\1>\s*}{32}s, 'scalar 2.1 mapped OK');
+ok(s{\s*<(hex)>0x20</\1>\s*}{32}s, 'scalar 2.1 mapped OK');
+ok(s{\s*<(word)>thirty two</\1>\s*}{32}s, 'scalar 2.1 mapped OK');
+ok(s{\s*<(number)>323232</\1>\s*}{NUM}s, 'element 2 OK');
+like($_, qr{^<(\w+)\s*>NUMNUM</\1>$}, 'document OK');
+
+
+# Check grouped tags get ungrouped correctly
+
+$ref = {
+  prefix => 'before',
+  dirs   => [ '/usr/bin', '/usr/local/bin' ],
+  suffix => 'after',
+};
+
+# Expect:
+#
+# <opt>
+#   <prefix>before</prefix>
+#   <dirs>
+#     <dir>/usr/bin</dir>
+#     <dir>/usr/local/bin</dir>
+#   </dirs>
+#   <suffix>after</suffix>
+# </opt>
+#
+
+$@ = '';
+$_ = eval { XMLout($ref, grouptags => {dirs => 'dirs'}, noattr => 1); };
+ok($@, 'bad GroupTags value was caught');
+like("$@", qr{Bad value in GroupTags: 'dirs' => 'dirs'},
+  'error message looks good');
+
+$@ = '';
+$_ = eval { XMLout($ref, grouptags => {dirs => 'dir'}, noattr => 1); };
+ok(!$@, 'good GroupTags value caused no error');
+
+ok(s{\s*<(prefix)>before</\1>\s*}{ELEM}s, 'prefix OK');
+ok(s{\s*<(suffix)>after</\1>\s*}{ELEM}s,  'suffix OK');
+ok(s{\s*<dir>/usr/bin</dir>\s*<dir>/usr/local/bin</dir>\s*}{LIST}s,  'list OK');
+ok(s{\s*<dirs>LIST</dirs>\s*}{ELEM}s,  'group OK');
+like($_, qr{^<(\w+)\s*>ELEMELEMELEM</\1>$}, 'document OK');
+
+is_deeply($ref, {
+  prefix => 'before',
+  dirs   => [ '/usr/bin', '/usr/local/bin' ],
+  suffix => 'after',
+}, 'original ref is not messed with');
+
+# Try again with multiple groupings
+
+$ref = {
+  dirs   => [ '/usr/bin', '/usr/local/bin' ],
+  terms  => [ 'vt100', 'xterm' ],
+};
+
+# Expect:
+#
+# <opt>
+#   <dirs>
+#     <dir>/usr/bin</dir>
+#     <dir>/usr/local/bin</dir>
+#   </dirs>
+#   <terms>
+#     <term>vt100</term>
+#     <term>xterm</term>
+#   </terms>
+# </opt>
+#
+
+$_ = XMLout($ref, grouptags => {dirs => 'dir', terms => 'term'}, noattr => 1);
+
+ok(s{\s*<dir>/usr/bin</dir>\s*<dir>/usr/local/bin</dir>\s*}{LIST}s,  'list 1 OK');
+ok(s{\s*<dirs>LIST</dirs>\s*}{ELEM}s,  'group 1 OK');
+ok(s{\s*<term>vt100</term>\s*<term>xterm</term>\s*}{LIST}s,  'list 2 OK');
+ok(s{\s*<terms>LIST</terms>\s*}{ELEM}s,  'group 2 OK');
+like($_, qr{^<(\w+)\s*>ELEMELEM</\1>$}, 'document OK');
+
+
+# Confirm unfolding and grouping work together
+
+$ref = {
+  dirs   => {
+              first   => { content => '/usr/bin'       }, 
+              second  => { content => '/usr/local/bin' },
+            },
+};
+
+# Expect:
+#
+# <opt>
+#   <dirs>
+#     <dir name="first">/usr/bin</dir>
+#     <dir name="second">/usr/local/bin</dir>
+#   </dirs>
+# </opt>
+#
+
+$_ = XMLout($ref, 
+  grouptags => {dirs => 'dir'}, keyattr => {dir => 'name'},
+);
+
+ok(s{\s*<dir\s+name="first">/usr/bin</dir>\s*}{ITEM}s, 'item 1 OK');
+ok(s{\s*<dir\s+name="second">/usr/local/bin</dir>\s*}{ITEM}s, 'item 2 OK');
+ok(s{\s*<dirs>ITEMITEM</dirs>\s*}{GROUP}s,  'group OK');
+like($_, qr{^<(\w+)\s*>GROUP</\1>$}, 'document OK');
+
+
+# Combine unfolding, grouping and stripped content - watch it fail :-(
+
+$ref = {
+  dirs   => {
+              first   => '/usr/bin',
+              second  => '/usr/local/bin'
+            },
+};
+
+# Expect:
+#
+# <opt>
+#   <dirs first="/usr/bin" second="/usr/local/bin" />
+# </opt>
+#
+
+$_ = XMLout($ref, 
+  grouptags => {dirs => 'dir'}, keyattr => {dir => 'name'}, 
+  contentkey => '-content'
+);
+
+like($_, qr{
+  ^<(\w+)>\s*
+    <dirs>\s*
+      <dir
+        (?:
+          \s+first="/usr/bin"
+         |\s+second="/usr/local/bin"
+        ){2}\s*
+      />\s*
+    </dirs>\s*
+  </\1>$
+}x, 'Failed to unwrap/group stripped content - as expected');
+
+
+# Check 'NoIndent' option
+
+$ref = {
+  nest   => [ qw(one two three) ]
+};
+
+# Expect:
+#
+# <opt><nest>one</nest><nest>two</nest><nest>three</nest></opt>
+#
+
+$_ = XMLout($ref, NoIndent => 1);
+
+is_deeply(XMLin($_), $ref, 'parses ok');
+is($_, '<opt><nest>one</nest><nest>two</nest><nest>three</nest></opt>',
+'NoIndent worked ok');
+
+
+# Check 'NoIndent' works with KeyAttr
+
+$ref = {
+  person => {
+    bob  => { age => 25 },
+    kate => { age => 22 },
+  },
+};
+
+# Expect:
+#
+# <opt><person name="bob" age="25"><person name="kate" age="22"></opt>
+#
+
+$_ = XMLout($ref, NoIndent => 1, KeyAttr => {person => 'name'});
+
+is_deeply(XMLin($_), $ref, 'parses ok');
+like($_, qr{
+  <opt>
+    (
+    <person(\s+name="bob"|\s+age="25"){2}\s*/>
+    |<person(\s+name="kate"|\s+age="22"){2}\s*/>
+    ){2}
+  </opt>
+}sx,
+'NoIndent worked ok with KeyAttr');
+
+
+# Try the 'AttrIndent' option (assume NoSort defaults to off)
+
+$ref = {
+  beta => '2', 
+  gamma => '3', 
+  alpha => '1', 
+  colours => {
+    red => '#ff0000',
+    green => '#00ff00',
+  }
+};
+
+$_ = XMLout($ref, AttrIndent => 1, RootName => 'opt');
+
+is($_, '<opt alpha="1"
+     beta="2"
+     gamma="3">
+  <colours green="#00ff00"
+           red="#ff0000" />
+</opt>
+', 'AttrIndent seems to work');
+
+
+# Test the attribute/element sorting algorithm
+
+$xml = q{
+<opt>
+  <test id="beta"  animal="elephant" vegetable="carrot" />
+  <test id="gamma" animal="tiger"    vegetable="turnip" />
+  <test id="alpha" animal="giraffe"  vegetable="pumpkin" />
+  <box size="small" key="a" />
+  <box size="medium" id="b" />
+</opt>
+};
+
+$ref = XMLin($xml);
+
+$_ = XMLout($ref, RootName => 'opt');
+
+is($_, qq(<opt>\n) .
+       qq(  <box name="a" size="small" />\n) .
+       qq(  <box name="b" size="medium" />\n) .
+       qq(  <test name="alpha" animal="giraffe" vegetable="pumpkin" />\n) .
+       qq(  <test name="beta" animal="elephant" vegetable="carrot" />\n) .
+       qq(  <test name="gamma" animal="tiger" vegetable="turnip" />\n) .
+       qq(</opt>\n),
+'sorting by default key attribute works');
+
+
+# Try again but with specific key fields:
+
+$ref = XMLin($xml, KeyAttr => {test => 'vegetable', box => 'size'});
+
+$_ = XMLout($ref,
+  RootName => 'opt', 
+  KeyAttr => {test => 'vegetable', box => 'size'}
+);
+
+is($_, qq(<opt>\n) .
+       qq(  <box size="medium" id="b" />\n) .
+       qq(  <box size="small" key="a" />\n) .
+       qq(  <test vegetable="carrot" animal="elephant" id="beta" />\n) .
+       qq(  <test vegetable="pumpkin" animal="giraffe" id="alpha" />\n) .
+       qq(  <test vegetable="turnip" animal="tiger" id="gamma" />\n) .
+       qq(</opt>\n),
+'sorting by specified key attributes works');
+
+
+# Try again but with no key fields:
+
+$ref = XMLin($xml, KeyAttr => {});
+
+$_ = XMLout($ref, RootName => 'opt', KeyAttr => {});
+
+like($_, qr{^<opt>\s*
+  (
+    (
+      <test\s+animal="elephant"\s+id="beta"\s+vegetable="carrot"\s*/>\s*
+      <test\s+animal="tiger"\s+id="gamma"\s+vegetable="turnip"\s*/>\s*
+      <test\s+animal="giraffe"\s+id="alpha"\s+vegetable="pumpkin"\s*/>\s*
+    )
+    |(
+      <box\s+key="a"\s+size="small"\s*/>\s*
+      <box\s+id="b"\s+size="medium"\s*/>\s*
+    )
+  ){2}
+</opt>\s*
+$}sx, 'sorting with no key attribute works');
+
+
+# Check that sorting can be disabled
+
+$@ = '';
+SKIP: {
+  eval { require Tie::IxHash };
+
+  skip "Tie::IxHash not installed", 1 if $@;
+
+  my(%hash1, %hash2);
+  tie %hash1, 'Tie::IxHash', Jan => 1, Feb => 2, Mar => 3, Apr => 4, May => 5;
+  tie %hash2, 'Tie::IxHash', X => { b => 2 }, A => { c => 3 }, Z => { a => 1 },
+                             M => { f => 6 }, K => { e => 4 }, O => { d => 5 };
+  $hash1{func} = \%hash2;
+
+  $_ = XMLout(\%hash1, NoSort => 1, KeyAttr => {func => 'name'});
+
+  like($_, qr{
+    ^<opt\sJan="1"\sFeb="2"\sMar="3"\sApr="4"\sMay="5">\s+
+      <func(\sb="2"|\sname="X"){2}\s/>\s+
+      <func(\sc="3"|\sname="A"){2}\s/>\s+
+      <func(\sa="1"|\sname="Z"){2}\s/>\s+
+      <func(\sf="6"|\sname="M"){2}\s/>\s+
+      <func(\se="4"|\sname="K"){2}\s/>\s+
+      <func(\sd="5"|\sname="O"){2}\s/>\s+
+    </opt>\s*$
+  }sx, 'Suppressing sort worked');
+
+}
+
+# Check ValueAttr => {} can expand the relevant records
+
+$ref = { one => 1, two => 2, six => 6 };
+
+$xml = XMLout($ref, ValueAttr => { one => 'value', six => 'num' });
+
+like($xml, qr{
+    ^<opt\s+two="2"\s*>
+      (
+        \s*<one\s+value="1"\s*/>
+      | \s*<six\s+num="6"\s*/>
+      ){2}
+    \s*</opt>$
+  }sx, 'Correct attributes inserted when ValueAttr specified'
+);
+
+# Try out the NumericEscape option
+
+SKIP: {
+    skip "Perl 5.6 or better required", 4 unless($] >= 5.006);
+    
+    $ref = { euro => "\x{20AC}", nbsp => "\x{A0}" };
+
+    $xml = XMLout($ref);   # Default: no numeric escaping
+    my $ents = join ',', sort ($xml =~ m{&#(\d+);}g);
+    is($ents, '', "No numeric escaping by default");
+
+    $xml = XMLout($ref, NumericEscape => 0);
+    $ents = join ',', sort ($xml =~ m{&#(\d+);}g);
+    is($ents, '', "No numeric escaping: explicit");
+
+    $xml = XMLout($ref, NumericEscape => 2);
+    $ents = join ',', sort ($xml =~ m{&#(\d+);}g);
+    is($ents, '160,8364', "Level 2 numeric escaping looks good");
+
+    $xml = XMLout($ref, NumericEscape => 1);
+    $ents = join ',', sort ($xml =~ m{&#(\d+);}g);
+    is($ents, '8364', "Level 1 numeric escaping looks good");
+}
+
+# 'Stress test' with a data structure that maps to several thousand elements.
+# Unfold elements with XMLout() and fold them up again with XMLin()
+
+my $opt1 =  {};
+foreach my $i (0..40) {
+  foreach my $j (0..$i) {
+    $opt1->{TypeA}->{$i}->{Record}->{$j} = { Hex => sprintf("0x%04X", $j) };
+    $opt1->{TypeB}->{$i}->{Record}->{$j} = { Oct => sprintf("%04o", $j) };
+    $opt1->{List}->[$i]->[$j] = "$i:$j";
+  }
+}
+
+$xml = XMLout($opt1, keyattr => { TypeA => 'alpha', TypeB => 'beta', Record => 'id' });
+
+my $opt2 = XMLin($xml, keyattr => { TypeA => 'alpha', TypeB => 'beta', Record => 'id' }, forcearray => 1);
+
+is_deeply($opt1, $opt2, 'large datastructure mapped to XML and back OK');
+
+exit(0);
+
+
+
diff --git a/t/3_Storable.t b/t/3_Storable.t
new file mode 100644 (file)
index 0000000..88d48af
--- /dev/null
@@ -0,0 +1,238 @@
+# $Id: 3_Storable.t,v 1.11 2007/08/02 10:38:22 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+
+$^W = 1;
+
+
+eval { require Storable; };
+unless($INC{'Storable.pm'}) {
+  plan skip_all => 'no Storable.pm';
+}
+unless(UNIVERSAL::can(Storable => 'lock_nstore')) {
+  plan skip_all => 'Storable.pm is too old - no file locking support';
+}
+
+
+# Initialise filenames and check they're there
+
+my $SrcFile   = File::Spec->catfile('t', 'desertnet.src');
+my $XMLFile   = File::Spec->catfile('t', 'desertnet.xml');
+my $CacheFile = File::Spec->catfile('t', 'desertnet.stor');
+
+unless(-e $SrcFile) {
+  plan skip_all => 'test data missing';
+}
+
+# Make sure we can write to the filesystem and check it uses the same
+# clock as the machine we're running on.
+
+my $t0 = time();
+unless(open(XML, ">$XMLFile")) {
+  plan skip_all => "can't create test file: $!";
+}
+close(XML);
+my $t1 = (stat($XMLFile))[9];
+my $t2 = time();
+
+if($t1 < $t0  or  $t2 < $t1) {
+  plan skip_all => 'time moved backwards!'
+}
+
+
+plan tests => 23;
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Copy a file
+#
+
+sub CopyFile {
+  my($Src, $Dst) = @_;
+  
+  open(IN, $Src) || return(undef);
+  local($/) = undef;
+  my $Data = <IN>;
+  close(IN);
+
+  open(OUT, ">$Dst") || return(undef);
+  print OUT $Data;
+  close(OUT);
+
+  return(1);
+}
+
+
+##############################################################################
+# Delete a file - portably
+#
+
+sub DeleteFile {
+  my($Filename) = @_;
+
+  if ('VMS' eq $^O) {
+    1 while (unlink($Filename));
+  } else {
+    unlink($Filename);
+  }
+}
+
+
+##############################################################################
+# Create a file, making sure that its timestamp is newer than another
+# existing file.
+#
+
+sub MakeNewerFile {
+  my($File1, $File2, $CodeRef) = @_;
+
+  my $t0 = (stat($File1))[9];
+  while(1) {
+    unlink($File2);
+    $CodeRef->();
+    return if (stat($File2))[9] > $t0;
+    sleep(1);
+  }
+}
+
+
+##############################################################################
+# Wait until the current time is greater than the supplied value
+#
+
+sub PassTime {
+  my($Target) = @_;
+
+  while(time <= $Target) {
+    sleep 1;
+  }
+}
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+use XML::Simple;
+
+# Initialise test data
+
+my $Expected  = {
+          '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'
+                                                     ]
+                                      }
+                      }
+        };
+
+ok(CopyFile($SrcFile, $XMLFile), 'copied known good source file');
+unlink($CacheFile);
+ok(! -e $CacheFile, 'no cache files lying around');
+
+my $opt = XMLin($XMLFile);
+is_deeply($opt, $Expected, 'parsed expected data from file');
+ok(! -e $CacheFile, 'and no cache file was created');
+PassTime(time());                     # Ensure cache file will be newer
+
+$opt = XMLin($XMLFile, cache => 'storable');
+is_deeply($opt, $Expected, 'parsed expected data from file (again)');
+ok(-e $CacheFile, 'but this time a cache file was created');
+$t0 = (stat($CacheFile))[9];       # Remember cache timestamp
+PassTime($t0);
+
+$opt = XMLin($XMLFile, cache => ['storable']);
+is_deeply($opt, $Expected, 'got expected data from cache');
+$t1 = (stat($CacheFile))[9];
+is($t0, $t1, 'and cache timestamp has not changed');
+
+PassTime(time());
+$t0 = time();
+open(FILE, ">>$XMLFile");             # Touch the XML file
+print FILE "\n";
+close(FILE);
+$opt = XMLin($XMLFile, cache => 'storable');
+is_deeply($opt, $Expected, 'parsed in expected value again');
+$t2 = (stat($CacheFile))[9];
+isnt($t1, $t2, 'and this time the cache timestamp has changed');
+
+DeleteFile($XMLFile);
+ok(! -e $XMLFile, 'deleted the source file');
+open(FILE, ">$XMLFile");              # Re-create it (empty)
+close(FILE);
+ok(-e $XMLFile, 'recreated the source file');
+is(-s $XMLFile, 0, 'but with nothing in it');
+MakeNewerFile($XMLFile, $CacheFile, sub { # Make sure cache file is newer
+  Storable::nstore($Expected, $CacheFile);
+});
+$opt = XMLin($XMLFile, cache => 'storable');
+is_deeply($opt, $Expected, 'got the expected data from the cache');
+$t2 = (stat($CacheFile))[9];
+PassTime($t2);
+open(FILE, ">$XMLFile") ||            # Write some new data to the XML file
+  die "open(>$XMLFile): $!\n";
+print FILE qq(<opt one="1" two="2"></opt>\n);
+close(FILE);
+
+$opt = XMLin($XMLFile);               # Parse with no caching
+is_deeply($opt, { one => 1, two => 2}, 'parsed in expected data from file');
+$t0 = (stat($CacheFile))[9];          # And timestamp on cache file
+my $s0 = (-s $CacheFile);
+is($t0, $t2, 'and the cache file was not touched');
+
+                                      # Parse again with caching enabled
+$opt = XMLin($XMLFile, cache => 'storable');
+is_deeply($opt, { one => 1, two => 2}, 'parsed expected data through cache');
+$t1 = (stat($CacheFile))[9];
+my $s1 = (-s $CacheFile);
+ok(($t0 != $t1) || ($s0 != $s1),
+'and the cache was updated'); # Content changes but date may not on Win32
+
+ok(CopyFile($SrcFile, $XMLFile), 'copied back the original file');
+PassTime($t1);
+$opt = XMLin($XMLFile, cache => 'storable');
+is_deeply($opt, $Expected, 'parsed expected data in through cache');
+
+# Make sure scheme name is case-insensitive
+
+$opt = XMLin($XMLFile, cache => 'Storable');
+is_deeply($opt, $Expected, 'scheme name is case-insensitive');
+
+# Make sure bad scheme names are trapped
+
+$@='';
+$_ = eval { XMLin($XMLFile, cache => 'Storubble'); };
+is($_, undef, 'bad cache scheme names are trapped');
+like($@, qr/Unsupported caching scheme: storubble/,
+'with correct error message');
+
+
+# Clean up and go
+
+unlink($CacheFile);
+unlink($XMLFile);
+exit(0);
+
diff --git a/t/4_MemShare.t b/t/4_MemShare.t
new file mode 100644 (file)
index 0000000..3e3810a
--- /dev/null
@@ -0,0 +1,154 @@
+# $Id: 4_MemShare.t,v 1.5 2005/01/29 04:17:42 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+
+$^W = 1;
+
+
+# Initialise filenames and check they're there
+
+my $SrcFile   = File::Spec->catfile('t', 'desertnet.src');
+my $XMLFile   = File::Spec->catfile('t', 'desertnet.xml');
+
+unless(-e $SrcFile) {
+  plan skip_all => 'test data missing';
+}
+
+# Make sure we can write to the filesystem and check it uses the same
+# clock as the machine we're running on.
+
+my $t0 = time();
+unless(open(XML, ">$XMLFile")) {
+  plan skip_all => "can't create test file: $!";
+}
+close(XML);
+my $t1 = (stat($XMLFile))[9];
+my $t2 = time();
+
+if($t1 < $t0  or  $t2 < $t1) {
+  plan skip_all => 'time moved backwards!'
+}
+
+
+plan tests => 8;
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Copy a file
+#
+
+sub CopyFile {
+  my($Src, $Dst) = @_;
+  
+  open(IN, $Src) || return(undef);
+  local($/) = undef;
+  my $Data = <IN>;
+  close(IN);
+
+  open(OUT, ">$Dst") || return(undef);
+  print OUT $Data;
+  close(OUT);
+
+  return(1);
+}
+
+
+##############################################################################
+# Wait until the current time is greater than the supplied value
+#
+
+sub PassTime {
+  my($Target) = @_;
+
+  while(time <= $Target) {
+    sleep 1;
+  }
+}
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+use XML::Simple;
+
+# Initialise test data
+
+my $Expected  = {
+          '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'
+                                                     ]
+                                      }
+                      }
+        };
+
+ok(CopyFile($SrcFile, $XMLFile), 'copied known good source file');
+$t0 = (stat($XMLFile))[9];         # Remember its timestamp
+                                      
+                                     # Parse it with caching enabled
+my $opt = XMLin($XMLFile, cache => 'memshare');
+is_deeply($opt, $Expected, 'parsed expected data from file');
+
+if ('VMS' eq $^O) {
+  1 while (unlink($XMLFile));
+} else {
+  unlink($XMLFile);
+}
+ok(! -e $XMLFile, 'deleted the XML source file');
+open(FILE, ">$XMLFile");              # Re-create it (empty)
+close(FILE);
+ok(-e $XMLFile, 'and recreated it (empty)');
+$t1 = $t0 - 1;
+eval { utime($t1, $t1, $XMLFile); };  # but wind back the clock
+$t2 = (stat($XMLFile))[9];         # Skip these tests if that didn't work
+SKIP: {
+  skip 'no utime', 2 if($t2 >= $t0);
+
+  $opt = XMLin($XMLFile, cache => 'memshare');
+  is_deeply($opt, $Expected, 'got expected values from the cache');
+  is(-s $XMLFile, 0, 'even though the XML file is empty');
+}
+PassTime(time());                      # Ensure timestamp changes
+
+open(FILE, ">$XMLFile");               # Write some new data to the XML file
+print FILE qq(<opt one="1" two="2"></opt>\n);
+close(FILE);
+PassTime(time());                      # Ensure current time later than file time
+
+                                       # Parse again with caching enabled
+$opt = XMLin($XMLFile, cache => 'memshare');
+is_deeply($opt, { one => 1, two => 2}, 'parsed new data through cache');
+
+$opt->{three} = 3;                     # Alter the returned structure
+                                       # Retrieve again from the cache
+my $opt2 = XMLin($XMLFile, cache => 'memshare');
+
+is($opt2->{three}, 3, 'cache was modified');
+
+
+exit(0);
+
diff --git a/t/5_MemCopy.t b/t/5_MemCopy.t
new file mode 100644 (file)
index 0000000..26d2d1d
--- /dev/null
@@ -0,0 +1,162 @@
+# $Id: 5_MemCopy.t,v 1.6 2005/01/29 04:17:42 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+
+$^W = 1;
+
+
+eval { require Storable; };
+unless($INC{'Storable.pm'}) {
+  plan skip_all => 'no Storable.pm';
+}
+
+# Initialise filenames and check they're there
+
+my $SrcFile   = File::Spec->catfile('t', 'desertnet.src');
+my $XMLFile   = File::Spec->catfile('t', 'desertnet.xml');
+
+unless(-e $SrcFile) {
+  plan skip_all => 'test data missing';
+}
+
+# Make sure we can write to the filesystem and check it uses the same
+# clock as the machine we're running on.
+
+my $t0 = time();
+unless(open(XML, ">$XMLFile")) {
+  plan skip_all => "can't create test file: $!";
+}
+close(XML);
+my $t1 = (stat($XMLFile))[9];
+my $t2 = time();
+
+if($t1 < $t0  or  $t2 < $t1) {
+  plan skip_all => 'time moved backwards!'
+}
+
+plan tests => 7;
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Copy a file
+#
+
+sub CopyFile {
+  my($Src, $Dst) = @_;
+  
+  open(IN, $Src) || return(undef);
+  local($/) = undef;
+  my $Data = <IN>;
+  close(IN);
+
+  open(OUT, ">$Dst") || return(undef);
+  print OUT $Data;
+  close(OUT);
+
+  return(1);
+}
+
+
+##############################################################################
+# Wait until the current time is greater than the supplied value
+#
+
+sub PassTime {
+  my($Target) = @_;
+
+  while(time <= $Target) {
+    sleep 1;
+  }
+}
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+use XML::Simple;
+
+# Initialise test data
+
+my $Expected  = {
+          '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'
+                                                     ]
+                                      }
+                      }
+        };
+
+ok(CopyFile($SrcFile, $XMLFile), 'copied source XML file');
+$t0 = (stat($XMLFile))[9];         # Remember its timestamp
+                                      
+                                     # Parse it with caching enabled
+my $opt = XMLin($XMLFile, cache => 'memcopy');
+is_deeply($opt, $Expected, 'parsed expected data through the cache');
+
+if ('VMS' eq $^O) {
+  1 while (unlink($XMLFile));
+} else {
+  unlink($XMLFile);
+}
+ok(! -e $XMLFile, 'deleted the source XML file');
+open(FILE, ">$XMLFile");              # Re-create it (empty)
+close(FILE);
+$t1 = $t0 - 1;
+eval { utime($t1, $t1, $XMLFile); };   # but wind back the clock
+$t2 = (stat($XMLFile))[9];         # Skip these tests if that didn't work
+SKIP: {
+  skip 'no utime', 2 if($t2 >= $t0);
+
+  $opt = XMLin($XMLFile, cache => 'memcopy');
+  is_deeply($opt, $Expected, 'got what we expected from the cache');
+  is(-s $XMLFile, 0, 'even though the source XML file is empty');
+}
+
+
+PassTime(time());                     # Ensure source file will be newer
+open(FILE, ">$XMLFile");              # Write some new data to the XML file
+print FILE qq(<opt one="1" two="2"></opt>\n);
+close(FILE);
+PassTime(time());                     # Ensure current time later than file time
+
+
+                                      # Parse again with caching enabled
+$opt = XMLin($XMLFile, cache => 'memcopy');
+is_deeply($opt, { one => 1, two => 2}, 'parsed expected data through cache');
+
+$opt->{three} = 3;                    # Alter the returned structure
+                                      # Retrieve again from the cache
+my $opt2 = XMLin($XMLFile, cache => 'memcopy');
+
+ok(!defined($opt2->{three}), 'cache not modified');
+
+
+# Clean up and go
+
+unlink($XMLFile);
+exit(0);
+
diff --git a/t/6_ObjIntf.t b/t/6_ObjIntf.t
new file mode 100644 (file)
index 0000000..7b95292
--- /dev/null
@@ -0,0 +1,383 @@
+# $Id: 6_ObjIntf.t,v 1.8 2004/02/29 09:49:18 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+
+$^W = 1;
+
+use Test::More tests => 37;
+
+##############################################################################
+# Derived version of XML::Simple that returns everything in upper case
+##############################################################################
+
+package XML::Simple::UC;
+
+use vars qw(@ISA);
+@ISA = qw(XML::Simple);
+
+sub build_tree {
+  my $self = shift;
+
+  my $tree = $self->SUPER::build_tree(@_);
+
+  ($tree) = uctree($tree);
+
+  return($tree);
+}
+
+sub uctree {
+  foreach my $i (0..$#_) {
+    my $x = $_[$i];
+    if(ref($x) eq 'ARRAY') {
+      $_[$i] = [ uctree(@$x) ];
+    }
+    elsif(ref($x) eq 'HASH') {
+      $_[$i] = { uctree(%$x) };
+    }
+    else {
+      $_[$i] = uc($x);
+    }
+  }
+  return(@_);
+}
+
+
+##############################################################################
+# Derived version of XML::Simple that uses CDATA sections for escaping
+##############################################################################
+
+package XML::Simple::CDE;
+
+use vars qw(@ISA);
+@ISA = qw(XML::Simple);
+
+sub escape_value {
+  my $self = shift;
+
+  my($data) = @_;
+
+  if($data =~ /[&<>"]/) {
+    $data = '<![CDATA[' . $data . ']]>';
+  }
+
+  return($data);
+}
+
+
+##############################################################################
+# Start of the test script itself
+##############################################################################
+
+package main;
+
+use XML::Simple;
+
+# Check error handling in constructor
+
+$@='';
+$_ = eval { XML::Simple->new('searchpath') };
+is($_, undef, 'invalid number of options are trapped');
+like($@, qr/Default options must be name=>value pairs \(odd number supplied\)/,
+'with correct error message');
+
+
+my $xml = q(<cddatabase>
+  <disc id="9362-45055-2" cddbid="960b750c">
+    <artist>R.E.M.</artist>
+    <album>Automatic For The People</album>
+    <track number="1">Drive</track>
+    <track number="2">Try Not To Breathe</track>
+    <track number="3">The Sidewinder Sleeps Tonite</track>
+    <track number="4">Everybody Hurts</track>
+    <track number="5">New Orleans Instrumental No. 1</track>
+    <track number="6">Sweetness Follows</track>
+    <track number="7">Monty Got A Raw Deal</track>
+    <track number="8">Ignoreland</track>
+    <track number="9">Star Me Kitten</track>
+    <track number="10">Man On The Moon</track>
+    <track number="11">Nightswimming</track>
+    <track number="12">Find The River</track>
+  </disc>
+</cddatabase>
+);
+
+my %opts1 = (
+  keyattr => { disc => 'cddbid', track => 'number' },
+  keeproot => 1, 
+  contentkey => 'title',
+  forcearray => [ qw(disc album) ] 
+);
+
+my %opts2 = (
+  keyattr => { }
+);
+
+my %opts3 = (
+  keyattr => { disc => 'cddbid', track => 'number' },
+  keeproot => 1, 
+  contentkey => '-title',
+  forcearray => [ qw(disc album) ] 
+);
+
+my $xs1 = new XML::Simple( %opts1 );
+my $xs2 = new XML::Simple( %opts2 );
+my $xs3 = new XML::Simple( %opts3 );
+isa_ok($xs1, 'XML::Simple', 'object one');
+isa_ok($xs2, 'XML::Simple', 'object two');
+isa_ok($xs3, 'XML::Simple', 'object three');
+is_deeply(\%opts1, {
+  keyattr => { disc => 'cddbid', track => 'number' },
+  keeproot => 1, 
+  contentkey => 'title',
+  forcearray => [ qw(disc album) ] 
+}, 'options hash was not corrupted');
+
+my $exp1 = {
+  'cddatabase' => {
+    'disc' => {
+      '960b750c' => {
+        'id' => '9362-45055-2',
+        'album' => [ 'Automatic For The People' ],
+        'artist' => 'R.E.M.',
+        'track' => {
+          1  => { 'title' => 'Drive' },
+          2  => { 'title' => 'Try Not To Breathe' },
+          3  => { 'title' => 'The Sidewinder Sleeps Tonite' },
+          4  => { 'title' => 'Everybody Hurts' },
+          5  => { 'title' => 'New Orleans Instrumental No. 1' },
+          6  => { 'title' => 'Sweetness Follows' },
+          7  => { 'title' => 'Monty Got A Raw Deal' },
+          8  => { 'title' => 'Ignoreland' },
+          9  => { 'title' => 'Star Me Kitten' },
+          10 => { 'title' => 'Man On The Moon' },
+          11 => { 'title' => 'Nightswimming' },
+          12 => { 'title' => 'Find The River' }
+        }
+      }
+    }
+  }
+};
+
+my $ref1 = $xs1->XMLin($xml);
+is_deeply($ref1, $exp1, 'parsed expected data via object 1');
+
+
+# Try using the other object
+
+my $exp2 = {
+  'disc' => {
+    'album' => 'Automatic For The People',
+    'artist' => 'R.E.M.',
+    'cddbid' => '960b750c',
+    'id' => '9362-45055-2',
+    'track' => [
+      { 'number' => 1,  'content' => 'Drive' },
+      { 'number' => 2,  'content' => 'Try Not To Breathe' },
+      { 'number' => 3,  'content' => 'The Sidewinder Sleeps Tonite' },
+      { 'number' => 4,  'content' => 'Everybody Hurts' },
+      { 'number' => 5,  'content' => 'New Orleans Instrumental No. 1' },
+      { 'number' => 6,  'content' => 'Sweetness Follows' },
+      { 'number' => 7,  'content' => 'Monty Got A Raw Deal' },
+      { 'number' => 8,  'content' => 'Ignoreland' },
+      { 'number' => 9,  'content' => 'Star Me Kitten' },
+      { 'number' => 10, 'content' => 'Man On The Moon' },
+      { 'number' => 11, 'content' => 'Nightswimming' },
+      { 'number' => 12, 'content' => 'Find The River' }
+    ]
+  }
+};
+
+my $ref2 = $xs2->XMLin($xml);
+is_deeply($ref2, $exp2, 'parsed expected data via object 2');
+
+
+# Try using the third object
+
+my $exp3 = {
+  'cddatabase' => {
+    'disc' => {
+      '960b750c' => {
+        'id' => '9362-45055-2',
+        'album' => [ 'Automatic For The People' ],
+        'artist' => 'R.E.M.',
+        'track' => {
+          1  => 'Drive',
+          2  => 'Try Not To Breathe',
+          3  => 'The Sidewinder Sleeps Tonite',
+          4  => 'Everybody Hurts',
+          5  => 'New Orleans Instrumental No. 1',
+          6  => 'Sweetness Follows',
+          7  => 'Monty Got A Raw Deal',
+          8  => 'Ignoreland',
+          9  => 'Star Me Kitten',
+          10 => 'Man On The Moon',
+          11 => 'Nightswimming',
+          12 => 'Find The River'
+        }
+      }
+    }
+  }
+};
+
+my $ref3 = $xs3->XMLin($xml);
+is_deeply($ref3, $exp3, 'parsed expected data via object 3');
+
+
+# Confirm default options in object merge correctly with options as args
+
+$ref1 = $xs1->XMLin($xml, keyattr => [], forcearray => 0);
+
+is_deeply($ref1, {              # Parsed to what we expected
+  'cddatabase' => {
+    'disc' => {
+      'album' => 'Automatic For The People',
+      'id' => '9362-45055-2',
+      'artist' => 'R.E.M.',
+      'cddbid' => '960b750c',
+      'track' => [
+        { 'number' => 1,  'title' => 'Drive' },
+        { 'number' => 2,  'title' => 'Try Not To Breathe' },
+        { 'number' => 3,  'title' => 'The Sidewinder Sleeps Tonite' },
+        { 'number' => 4,  'title' => 'Everybody Hurts' },
+        { 'number' => 5,  'title' => 'New Orleans Instrumental No. 1' },
+        { 'number' => 6,  'title' => 'Sweetness Follows' },
+        { 'number' => 7,  'title' => 'Monty Got A Raw Deal' },
+        { 'number' => 8,  'title' => 'Ignoreland' },
+        { 'number' => 9,  'title' => 'Star Me Kitten' },
+        { 'number' => 10, 'title' => 'Man On The Moon' },
+        { 'number' => 11, 'title' => 'Nightswimming' },
+        { 'number' => 12, 'title' => 'Find The River' }
+      ]
+    }
+  }
+}, 'successfully merged options');
+
+
+# Confirm that default options in object still work as expected
+
+$ref1 = $xs1->XMLin($xml);
+is_deeply($ref1, $exp1, 'defaults were not affected by merge');
+
+
+# Confirm they work for output too
+
+$_ = $xs1->XMLout($ref1);
+
+ok(s{<track number="1">Drive</track>}                         {<NEST/>}, 't1');
+ok(s{<track number="2">Try Not To Breathe</track>}            {<NEST/>}, 't2');
+ok(s{<track number="3">The Sidewinder Sleeps Tonite</track>}  {<NEST/>}, 't3');
+ok(s{<track number="4">Everybody Hurts</track>}               {<NEST/>}, 't4');
+ok(s{<track number="5">New Orleans Instrumental No. 1</track>}{<NEST/>}, 't5');
+ok(s{<track number="6">Sweetness Follows</track>}             {<NEST/>}, 't6');
+ok(s{<track number="7">Monty Got A Raw Deal</track>}          {<NEST/>}, 't7');
+ok(s{<track number="8">Ignoreland</track>}                    {<NEST/>}, 't8');
+ok(s{<track number="9">Star Me Kitten</track>}                {<NEST/>}, 't9');
+ok(s{<track number="10">Man On The Moon</track>}              {<NEST/>}, 't10');
+ok(s{<track number="11">Nightswimming</track>}                {<NEST/>}, 't11');
+ok(s{<track number="12">Find The River</track>}               {<NEST/>}, 't12');
+ok(s{<album>Automatic For The People</album>}                 {<NEST/>}, 'ttl');
+ok(s{cddbid="960b750c"}{ATTR}, 'cddbid');
+ok(s{id="9362-45055-2"}{ATTR}, 'id');
+ok(s{artist="R.E.M."}  {ATTR}, 'artist');
+ok(s{<disc(\s+ATTR){3}\s*>(\s*<NEST/>){13}\s*</disc>}{<DISC/>}s, 'disc');
+ok(m{^\s*<(cddatabase)>\s*<DISC/>\s*</\1>\s*$}, 'database');
+
+
+# Confirm error when mandatory parameter missing
+
+$_ = eval {
+  $xs1->XMLout();
+};
+ok(!defined($_), 'XMLout() method call with no args proves fatal');
+like($@, qr/XMLout\(\) requires at least one argument/, 
+'with correct error message');
+
+
+# Check that overriding build_tree() method works
+
+$xml = q(<opt>
+  <server>
+    <name>Apollo</name>
+    <address>10 Downing Street</address>
+  </server>
+</opt>
+);
+
+my $xsp = new XML::Simple::UC();
+$ref1 = $xsp->XMLin($xml);
+is_deeply($ref1, {
+  'SERVER' => {
+    'NAME' => 'APOLLO',
+    'ADDRESS' => '10 DOWNING STREET'
+  }
+}, 'inheritance works with build_tree() overridden');
+
+
+# Check that overriding escape_value() method works
+
+my $ref = {
+  'server' => {
+    'address' => '12->14 "Puf&Stuf" Drive'
+  }
+};
+
+$xsp = new XML::Simple::CDE();
+
+$_ = $xsp->XMLout($ref);
+
+like($_, qr{<opt>\s*
+ <server\s+address="<!\[CDATA\[12->14\s+"Puf&Stuf"\s+Drive\]\]>"\s*/>\s*
+</opt>}xs, 'inheritance works with escape_value() overridden');
+
+
+# Check variables defined in the constructor don't get trounced for
+# subsequent parses
+
+$xs1 = XML::Simple->new(
+  contentkey => '-content', 
+  varattr    => 'xsvar',
+  variables  => { conf_dir => '/etc', log_dir => '/tmp' }
+);
+
+$xml = q(<opt>
+  <dir xsvar="log_dir">/var/log</dir>
+  <file name="config_file">${conf_dir}/appname.conf</file>
+  <file name="log_file">${log_dir}/appname.log</file>
+  <file name="debug_file">${log_dir}/appname.dbg</file>
+</opt>);
+
+my $opt = $xs1->XMLin($xml);
+is_deeply($opt, {
+  file => {
+    config_file => '/etc/appname.conf',
+    log_file    => '/var/log/appname.log',
+    debug_file  => '/var/log/appname.dbg',
+  },
+  dir           => { xsvar => 'log_dir',  content => '/var/log' },
+}, 'variables from XML merged with predefined variables');
+
+$xml = q(<opt>
+  <file name="config_file">${conf_dir}/appname.conf</file>
+  <file name="log_file">${log_dir}/appname.log</file>
+  <file name="debug_file">${log_dir}/appname.dbg</file>
+</opt>);
+
+$opt = $xs1->XMLin($xml);
+is_deeply($opt, {
+  file => {
+    config_file => '/etc/appname.conf',
+    log_file    => '/tmp/appname.log',
+    debug_file  => '/tmp/appname.dbg',
+  },
+}, 'variables from XML merged with predefined variables');
+
+# check that unknown options passed to the constructor are rejected
+
+$@ = undef;
+eval { $xs1 = XML::Simple->new(KeyAttr => {}, WibbleFlibble => 1) };
+ok(defined($@), "unrecognised option caught by constructor");
+like($@, qr/^Unrecognised option: WibbleFlibble at/,
+  "correct message in exception");
+
+exit(0);
diff --git a/t/7_SaxStuff.t b/t/7_SaxStuff.t
new file mode 100644 (file)
index 0000000..9986659
--- /dev/null
@@ -0,0 +1,282 @@
+# $Id: 7_SaxStuff.t,v 1.5 2005/01/29 04:17:42 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+use IO::File;
+
+$^W = 1;
+
+
+BEGIN {
+  unshift @INC, File::Spec->catfile('t', 'lib');
+
+  eval { require XML::SAX; };
+  if($@) {
+    plan skip_all => 'no XML::SAX';
+  }
+}
+
+use TagsToUpper;
+
+# Initialise filenames and check they're there
+
+my $SrcFile   = File::Spec->catfile('t', 'desertnet.src');
+my $XMLFile   = File::Spec->catfile('t', 'desertnet.xml');
+my $CacheFile = File::Spec->catfile('t', 'desertnet.stor');
+
+unless(-e $SrcFile) {
+  plan skip_all => 'test data missing';
+}
+
+
+plan tests => 14;
+
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Copy a file
+#
+
+sub CopyFile {
+  my($Src, $Dst) = @_;
+  
+  open(IN, $Src) || return(undef);
+  local($/) = undef;
+  my $Data = <IN>;
+  close(IN);
+
+  open(OUT, ">$Dst") || return(undef);
+  print OUT $Data;
+  close(OUT);
+
+  return(1);
+}
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+use XML::Simple;
+
+# Initialise test data
+
+my $Expected  = {
+  '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'
+                                            ]
+                             }
+             }
+};
+
+my $xml = '';
+
+
+# Force default behaviour of using SAX parser if it is available (which it
+# is or we wouldn't be here).
+
+$XML::Simple::PREFERRED_PARSER = '';
+
+ok(CopyFile($SrcFile, $XMLFile), 'created source XML file');
+if ('VMS' eq $^O) {
+  1 while (unlink($CacheFile));
+} else {
+  unlink($CacheFile);
+}
+ok(! -e $CacheFile, 'deleted old cache files');
+
+# Pass in a filename to check parse_uri()
+
+my $opt = XMLin($XMLFile);
+is_deeply($opt, $Expected, 'parsed expected value from file');
+
+
+# Pass in an IO::File object to test parse_file()
+
+my $fh = IO::File->new("<$XMLFile");
+isa_ok($fh, 'IO::File', '$fh');
+$opt = XMLin($fh);
+is_deeply($opt, $Expected, 'parsed expected value from IO::File object');
+$fh->close();
+
+
+# Pass in a string to test parse_string()
+
+if(open(XMLFILE, "<$XMLFile")) {
+  local($/) = undef;
+  $xml = <XMLFILE>;
+  close(XMLFILE);
+}
+$opt = XMLin($xml);
+is_deeply($opt, $Expected, 'parsed expected value from string');
+  
+
+# Pass in '-' for STDIN
+
+open(OLDSTDIN, "<&STDIN");
+close(STDIN);
+open(STDIN, "<$XMLFile");
+$opt = XMLin('-');
+is_deeply($opt, $Expected, "parsed expected value from STDIN ('-')");
+
+open(STDIN, "<&OLDSTDIN");
+close(OLDSTDIN);
+
+
+# Try using XML:Simple object as a SAX handler
+
+my $simple = XML::Simple->new();
+my $parser = XML::SAX::ParserFactory->parser(Handler => $simple);
+
+$opt = $parser->parse_uri($XMLFile);
+is_deeply($opt, $Expected,
+  'XML::Simple as a SAX handler returned expected value');
+
+
+# Try again but make sure options from the constructor are being used
+
+$simple = XML::Simple->new(
+  keyattr    => { server => 'osname' },
+  forcearray => ['address'],
+);
+$parser = XML::SAX::ParserFactory->parser(Handler => $simple);
+
+$opt = $parser->parse_uri($XMLFile);
+my $Expected2 = {
+  'server' => {
+               'irix' => {
+                           'address' => [ '10.0.0.102' ],
+                           'osversion' => '6.5',
+                           'name' => 'gobi'
+                         },
+               'solaris' => {
+                              'address' => [ '10.0.0.101', '10.0.1.101' ],
+                              'osversion' => '2.6',
+                              'name' => 'sahara'
+                            },
+               'linux' => {
+                            'address' => [ '10.0.0.103', '10.0.1.103' ],
+                            'osversion' => '2.0.34',
+                            'name' => 'kalahari'
+                          }
+             }
+};
+
+is_deeply($opt, $Expected2, 'options passed to handler contructor work');
+
+
+# Try using XML::Simple to drive a SAX pipeline
+
+my $Expected3  = {
+  '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'
+                                            ]
+                             }
+             }
+};
+my $simple2 = XML::Simple->new(keyattr => [qw(NAME)]);
+my $filter = TagsToUpper->new(Handler => $simple2);
+
+my $opt2 = XMLout($opt,
+  keyattr    => { server => 'osname' },
+  Handler    => $filter,
+);
+is_deeply($opt2, $Expected3, 'driving a SAX pipeline with XML::Simple worked');
+
+
+# Confirm that 'handler' is a synonym for 'Handler'
+
+$simple2 = XML::Simple->new(keyattr => [qw(NAME)]);
+$filter = TagsToUpper->new(Handler => $simple2);
+$opt2 = XMLout($opt,
+  keyattr    => { server => 'osname' },
+  handler    => $filter,
+);
+is_deeply($opt2, $Expected3, "'handler' is a synonym for 'Handler'");
+
+
+# Confirm that DataHandler routine gets called
+
+$xml = q(<opt><anon>one</anon><anon>two</anon><anon>three</anon></opt>);
+$simple = XML::Simple->new(
+  DataHandler => sub {
+                  my $xs = shift;
+                  my $data = shift;
+                  return(join(',', @$data));
+                }
+);
+$parser = XML::SAX::ParserFactory->parser(Handler => $simple);
+my $result = $parser->parse_string($xml);
+
+is($result, 'one,two,three', "'DataHandler' option works");
+
+
+# Confirm that 'datahandler' is a synonym for 'DataHandler'
+
+$simple = XML::Simple->new(
+  datahandler => sub {
+                  my $xs = shift;
+                  my $data = shift;
+                  return(join(',', reverse(@$data)));
+                }
+);
+$parser = XML::SAX::ParserFactory->parser(Handler => $simple);
+$result = $parser->parse_string($xml);
+
+is($result, 'three,two,one', "'datahandler' is a synonym for 'DataHandler'");
+
+
+# Confirm keeproot logic gets called
+
+$simple = XML::Simple->new(keeproot => 1);
+$parser = XML::SAX::ParserFactory->parser(Handler => $simple);
+$opt = $parser->parse_string('<opt a="1" b="2" />');
+is_deeply($opt, {opt => {a => 1, b => 2}}, "keeproot works with SAX pipelines");
+
+# Clean up and go
+
+unlink($CacheFile);
+unlink($XMLFile);
+exit(0);
+
diff --git a/t/8_Namespaces.t b/t/8_Namespaces.t
new file mode 100644 (file)
index 0000000..b4cef66
--- /dev/null
@@ -0,0 +1,230 @@
+# $Id: 8_Namespaces.t,v 1.7 2004/04/05 09:12:51 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+use IO::File;
+
+$^W = 1;
+
+
+eval { require XML::SAX; };
+if($@) {
+  plan skip_all => 'no XML::SAX';
+}
+
+eval { require XML::NamespaceSupport; };
+if($@) {
+  plan skip_all => "no XML::NamespaceSupport";
+}
+if($XML::NamespaceSupport::VERSION < 1.04) {
+  plan skip_all => "XML::NamespaceSupport is too old (upgrade to 1.04 or better)";
+}
+
+plan tests => 8;
+
+
+##############################################################################
+#                   S U P P O R T   R O U T I N E S
+##############################################################################
+
+##############################################################################
+# Copy a file
+#
+
+sub CopyFile {
+  my($Src, $Dst) = @_;
+  
+  open(IN, $Src) || return(undef);
+  local($/) = undef;
+  my $Data = <IN>;
+  close(IN);
+
+  open(OUT, ">$Dst") || return(undef);
+  print OUT $Data;
+  close(OUT);
+
+  return(1);
+}
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+use XML::Simple;
+
+# Force default behaviour of using SAX parser if it is available (which it
+# is or we wouldn't be here).
+
+$XML::Simple::PREFERRED_PARSER = '';
+
+# Confirm that by default qnames are not expanded on input
+
+my $xml = q(<config xmlns:perl="http://www.perl.com/">
+  <perl:list count="3" perl:type="array">
+    <item>one</item>
+    <item>two</item>
+    <item>three</item>
+    <test xmlns:perl="http://www.microsoft.com" perl:tm="trademark" />
+  </perl:list>
+</config>);
+
+my $expected = {
+  'perl:list' => {
+    'count' => '3',
+    'item' => [
+      'one',
+      'two',
+      'three'
+    ],
+    'perl:type' => 'array',
+    'test' => {
+      'xmlns:perl' => 'http://www.microsoft.com',
+      'perl:tm' => 'trademark',
+    }
+  },
+  'xmlns:perl' => 'http://www.perl.com/'
+};
+
+my $opt = XMLin($xml);
+is_deeply($opt, $expected, 'qnames are not expanded by default');
+
+
+# Try again with nsexpand option set
+
+$expected = {
+  '{http://www.perl.com/}list' => {
+    'count' => '3',
+    'item' => [
+      'one',
+      'two',
+      'three'
+    ],
+    '{http://www.perl.com/}type' => 'array',
+    'test' => {
+      '{http://www.microsoft.com}tm' => 'trademark',
+      '{http://www.w3.org/2000/xmlns/}perl' => 'http://www.microsoft.com'
+    }
+  },
+  '{http://www.w3.org/2000/xmlns/}perl' => 'http://www.perl.com/'
+};
+
+$opt = XMLin($xml, nsexpand => 1);
+is_deeply($opt, $expected, 'qnames are expanded on request');
+
+
+# Confirm that output expansion does not occur by default
+
+$opt = {
+  '{http://www.w3.org/2000/xmlns/}perl' => 'http://www.perl.com/',
+  '{http://www.perl.com/}attr' => 'value',
+  'bare' => 'Beer!',
+  '{http://www.perl.com/}element' => [ 'data' ],
+};
+
+$xml = XMLout($opt);
+like($xml, qr{
+  ^\s*<opt
+  (\s+{http://www.w3.org/2000/xmlns/}perl="http://www.perl.com/"
+  |\s+{http://www.perl.com/}attr="value"
+  |\s+bare="Beer!"){3}
+  \s*>
+  \s*<{http://www.perl.com/}element\s*>data</{http://www.perl.com/}element\s*>
+  \s*</opt>
+  \s*$
+}sx, 'clarkian names not converted to qnames on output by default');
+
+
+# Confirm nsexpand option works on output
+
+$xml = XMLout($opt, nsexpand => 1);
+ok($xml =~ m{
+  ^\s*<opt
+  (\s+xmlns:perl="http://www.perl.com/"
+  |\s+perl:attr="value"
+  |\s+bare="Beer!"){3}
+  \s*>
+  \s*<perl:element\s*>data</perl:element\s*>
+  \s*</opt>
+  \s*$
+}sx, 'clarkian names are converted to qnames on output on request');
+
+
+# Check that default namespace is correctly read in ...
+
+$xml = q(<opt xmlns="http://www.orgsoc.org/">
+  <list>
+    <member>Tom</member>
+    <member>Dick</member>
+    <member>Larry</member>
+  </list>
+</opt>
+);
+
+$expected = {
+  'xmlns' => 'http://www.orgsoc.org/',
+  '{http://www.orgsoc.org/}list' => {
+    '{http://www.orgsoc.org/}member' => [ 'Tom', 'Dick', 'Larry' ],
+  }
+};
+
+$opt = XMLin($xml, nsexpand => 1);
+is_deeply($opt, $expected, 'expansion of default namespace works');
+
+
+# ... and written out
+
+$xml = XMLout($opt, nsexpand => 1);
+like($xml, qr{
+  ^\s*<opt
+  \s+xmlns="http://www.orgsoc.org/"
+  \s*>
+  \s*<list>
+  \s*<member>Tom</member>
+  \s*<member>Dick</member>
+  \s*<member>Larry</member>
+  \s*</list>
+  \s*</opt>
+  \s*$
+}sx, 'default namespaces are output correctly too');
+
+
+# Check that the autogeneration of namespaces works as we expect
+
+$opt = {
+  'xmlns' => 'http://www.orgsoc.org/',
+  '{http://www.orgsoc.org/}list' => {
+    '{http://www.orgsoc.org/}member' => [ 'Tom', 'Dick', 'Larry' ],
+    '{http://www.phantom.com/}director' => [ 'Bill', 'Ben' ],
+  }
+};
+
+$xml = XMLout($opt, nsexpand => 1);
+my $prefix = '';
+if($xml =~ m{<list\s+xmlns:(\w+)="http://www.phantom.com/"\s*>}) {
+  $prefix = $1;
+}
+  # regex match split in two to workaround 5.8.1/utf8/regex match prob
+like($xml, qr{
+  \s*<opt
+  \s+xmlns="http://www.orgsoc.org/"
+  \s*>
+  .*?
+  </list>
+  \s*</opt>
+}sx, 'namespace prefixes are generated automatically (part 1)');
+
+like($xml, qr{
+  (\s*<member>Tom</member>
+   \s*<member>Dick</member>
+   \s*<member>Larry</member>
+  |\s*<${prefix}:director>Bill</${prefix}:director>
+   \s*<${prefix}:director>Ben</${prefix}:director>){2}
+  #\s*</list>
+}sx, 'namespace prefixes are generated automatically (part 2)');
+
+
+exit(0);
+
diff --git a/t/9_Strict.t b/t/9_Strict.t
new file mode 100644 (file)
index 0000000..48f213c
--- /dev/null
@@ -0,0 +1,341 @@
+# $Id: 9_Strict.t,v 1.6 2007/08/15 10:36:48 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+
+$^W = 1;
+
+plan tests => 40;
+
+
+##############################################################################
+#                      T E S T   R O U T I N E S
+##############################################################################
+
+eval "use XML::Simple qw(:strict);";
+ok(!$@, 'XML::Simple loads ok with qw(:strict)');
+
+# Check that the basic functionality still works
+
+my $xml = q(<opt name1="value1" name2="value2"></opt>);
+
+$@ = '';
+my $opt = eval {
+  XMLin($xml, forcearray => 1, keyattr => {});
+};
+is($@, '', 'XMLin() did not fail');
+
+my $keys = join(' ', sort keys %$opt);
+
+is($keys, 'name1 name2', 'and managed to produce the expected results');
+
+
+# Confirm that forcearray cannot be omitted
+
+eval {
+  $opt = XMLin($xml, keyattr => {});
+};
+
+isnt($@, '', 'omitting forcearray was a fatal error');
+like($@, qr/(?i)No value specified for 'forcearray'/, 
+  'with the correct error message');
+
+
+# Confirm that keyattr cannot be omitted
+
+eval {
+  $opt = XMLin($xml, forcearray => []);
+};
+
+isnt($@, '', 'omitting keyattr was a fatal error');
+like($@, qr/(?i)No value specified for 'keyattr'/,
+  'with the correct error message');
+
+
+# Confirm that element names from keyattr cannot be omitted from forcearray
+
+eval {
+  $opt = XMLin($xml, keyattr => { part => 'partnum' }, forcearray => 0);
+};
+
+isnt($@, '', 'omitting forcearray for elements in keyattr was a fatal error');
+like($@, qr/(?i)<part> set in keyattr but not in forcearray/,
+  'with the correct error message');
+
+
+eval {
+  $opt = XMLin($xml, keyattr => { part => 'partnum' }, forcearray => ['x','y']);
+};
+
+isnt($@, '', 'omitting keyattr elements from forcearray was a fatal error');
+like($@, qr/(?i)<part> set in keyattr but not in forcearray/,
+  'with the correct error message');
+
+
+# Confirm that missing key attributes are detected
+
+$xml = q(
+<opt>
+  <part partnum="12345" desc="Thingy" />
+  <part partnum="67890" desc="Wotsit" />
+  <part desc="Fnurgle" />
+</opt>
+);
+
+eval {
+  $opt = XMLin($xml, keyattr => { part => 'partnum' }, forcearray => 1);
+};
+
+isnt($@, '', 'key attribute missing from names element was a fatal error');
+like($@, qr/(?i)<part> element has no 'partnum' key attribute/,
+  'with the correct error message');
+
+
+# Confirm that non-unique values in key attributes are detected
+
+$xml = q(
+<opt>
+  <part partnum="12345" desc="Thingy" />
+  <part partnum="67890" desc="Wotsit" />
+  <part partnum="12345" desc="Springy" />
+</opt>
+);
+
+eval {
+  $opt = XMLin($xml, keyattr => { part => 'partnum' }, forcearray => 1);
+};
+
+isnt($@, '', 'non-unique key attribute values was a fatal error');
+like($@, qr/(?i)<part> element has non-unique value in 'partnum' key attribute: 12345/,
+  'with the correct error message');
+
+
+# Confirm that stringification of references is trapped
+
+$xml = q(
+<opt>
+  <item>
+    <name><firstname>Bob</firstname></name>
+    <age>21</age>
+  </item>
+</opt>
+);
+
+eval {
+  $opt = XMLin($xml, keyattr => { item => 'name' }, forcearray => ['item']);
+};
+
+isnt($@, '', 'key attribute not a scalar was a fatal error');
+like($@, qr/(?i)<item> element has non-scalar 'name' key attribute/,
+  'with the correct error message');
+
+
+##############################################################################
+# Now confirm that XMLout gets checked too
+#
+
+
+# Check that the basic functionality still works under :strict
+
+my $ref = {
+  person => [
+    { name => 'bob' },
+    { name => 'kate' },
+  ]
+};
+
+$@ = '';
+$xml = eval {
+  XMLout($ref, keyattr => {}, rootname => 'list');
+};
+is($@, '', 'XMLout() did not fail');
+
+like($xml, qr{
+      ^\s*<list\s*>
+      \s*<person\s+name="bob"\s*/>
+      \s*<person\s+name="kate"\s*/>
+      \s*</list\s*>\s*$
+    }xs, 'and managed to produce the expected results');
+
+
+# Confirm that keyattr cannot be omitted
+
+$@ = '';
+eval {
+  XMLout($ref, rootname => 'list');
+};
+
+isnt($@, '', 'omitting keyattr was a fatal error');
+like($@, qr/(?i)No value specified for 'keyattr'/,
+  'with the correct error message');
+
+
+# Confirm that forcearray can be omitted (only rqd on input)
+
+$@ = '';
+eval {
+  XMLout($ref, keyattr => {x => 'y'});
+};
+
+is($@, '', 'omitting forcearray was not a fatal error on output');
+
+
+##############################################################################
+# Now repeat all that using the OO syntax
+##############################################################################
+
+# Check that the basic functionality still works
+
+$xml = q(<opt name1="value1" name2="value2"></opt>);
+
+my $xs = XML::Simple->new(forcearray => 1, keyattr => {});
+
+$@ = '';
+$opt = eval {
+  $xs->XMLin($xml);
+};
+is($@, '', '$xs->XMLin() did not fail');
+
+$keys = join(' ', sort keys %$opt);
+
+is($keys, 'name1 name2', 'and managed to produce the expected results');
+
+# Confirm that forcearray cannot be omitted
+
+$xs = XML::Simple->new(keyattr => {});
+
+$@ = '';
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'omitting forcearray was a fatal error');
+like($@, qr/(?i)No value specified for 'forcearray'/, 
+  'with the correct error message');
+
+
+# Confirm that keyattr cannot be omitted
+
+$xs = XML::Simple->new(forcearray => []);
+
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'omitting keyattr was a fatal error');
+like($@, qr/(?i)No value specified for 'keyattr'/,
+  'with the correct error message');
+
+
+# Confirm that element names from keyattr cannot be omitted from forcearray
+
+$xs = XML::Simple->new(keyattr => { part => 'partnum' }, forcearray => 0);
+
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'omitting forcearray for elements in keyattr was a fatal error');
+like($@, qr/(?i)<part> set in keyattr but not in forcearray/,
+  'with the correct error message');
+
+
+$xs = XML::Simple->new(keyattr => { part => 'partnum' }, forcearray => ['x','y']);
+
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'omitting keyattr elements from forcearray was a fatal error');
+like($@, qr/(?i)<part> set in keyattr but not in forcearray/,
+  'with the correct error message');
+
+
+# Confirm that missing key attributes are detected
+
+$xml = q(
+<opt>
+  <part partnum="12345" desc="Thingy" />
+  <part partnum="67890" desc="Wotsit" />
+  <part desc="Fnurgle" />
+</opt>
+);
+
+$xs = XML::Simple->new(keyattr => { part => 'partnum' }, forcearray => 1);
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'key attribute missing from names element was a fatal error');
+like($@, qr/(?i)<part> element has no 'partnum' key attribute/,
+  'with the correct error message');
+
+
+# Confirm that stringification of references is trapped
+
+$xml = q(
+<opt>
+  <item>
+    <name><firstname>Bob</firstname></name>
+    <age>21</age>
+  </item>
+</opt>
+);
+
+$xs = XML::Simple->new(keyattr => { item => 'name' }, forcearray => ['item']);
+
+eval {
+  $xs->XMLin($xml);
+};
+
+isnt($@, '', 'key attribute not a scalar was a fatal error');
+like($@, qr/(?i)<item> element has non-scalar 'name' key attribute/,
+  'with the correct error message');
+
+
+##############################################################################
+# Now confirm that XMLout gets checked too
+#
+
+
+# Check that the basic functionality still works under :strict
+
+$ref = {
+  person => [
+    { name => 'bob' },
+    { name => 'kate' },
+  ]
+};
+
+$xs = XML::Simple->new(keyattr => {}, rootname => 'list');
+
+$@ = '';
+$xml = eval {
+  $xs->XMLout($ref);
+};
+is($@, '', 'XMLout() did not fail');
+
+like($xml, qr{
+      ^\s*<list\s*>
+      \s*<person\s+name="bob"\s*/>
+      \s*<person\s+name="kate"\s*/>
+      \s*</list\s*>\s*$
+    }xs, 'and managed to produce the expected results');
+
+
+# Confirm that keyattr cannot be omitted
+
+$xs = XML::Simple->new(rootname => 'list');
+
+eval {
+  $xs->XMLout($ref);
+};
+
+isnt($@, '', 'omitting keyattr was a fatal error');
+like($@, qr/(?i)No value specified for 'keyattr'/,
+  'with the correct error message');
+
+
+exit(0);
+
diff --git a/t/A_XMLParser.t b/t/A_XMLParser.t
new file mode 100644 (file)
index 0000000..ea33772
--- /dev/null
@@ -0,0 +1,128 @@
+# $Id: A_XMLParser.t,v 1.1 2004/02/29 09:49:18 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use IO::File;
+use File::Spec;
+
+$^W = 1;
+
+
+# Initialise filenames and check they're there
+
+my $XMLFile = File::Spec->catfile('t', 'test1.xml');  # t/test1.xml
+
+unless(-e $XMLFile) {
+  plan skip_all => 'Test data missing';
+}
+
+eval { require XML::Parser; };
+unless($INC{'XML/Parser.pm'}) {
+  plan skip_all => 'no XML::Parser';
+}
+
+plan tests => 14;
+
+use XML::Simple;
+
+my $last_warning = '';
+my $opt;
+
+
+# Use environment variable to set preferred parser
+
+$ENV{XML_SIMPLE_PREFERRED_PARSER} = 'XML::Parser';
+
+
+# Try using a SAX-only option
+
+{
+  local($SIG{__WARN__}) = \&warn_handler;
+
+  $@ = '';
+  $opt = eval { XMLin('<x y="z" />', nsexpand => 1) };
+}
+
+isnt($last_warning, '', "Parsing caused warning (as expected)");
+like($last_warning, qr/'nsexpand' option requires XML::SAX/, 
+  'Message contained expected text');
+is_deeply($opt, {y => 'z'}, "Parsing was successful");
+
+
+# Check for deprecation warning
+
+{
+  local($SIG{__WARN__}) = \&warn_handler;
+
+  $@ = '';
+  $last_warning = '';
+  $opt = eval { XMLin('<x y="z" />', ParserOpts => [ ParseParamEnt => 1 ]) };
+}
+
+isnt($last_warning, '', "Using ParserOpts caused warning (as expected)");
+like($last_warning, qr/'ParserOpts' is deprecated/, 
+  'Message contained expected text');
+is_deeply($opt, {y => 'z'}, "Parsing was successful");
+
+
+# Check it doesn't happen if warnings disabled
+
+{
+  local($SIG{__WARN__}) = \&warn_handler;
+
+  $@ = '';
+  $last_warning = '';
+  local($^W) = 0;
+  $opt = eval { XMLin('<x y="z" />', ParserOpts => [ ParseParamEnt => 1 ]) };
+}
+
+is($last_warning, '', "ParserOpts warning uppressed successfully");
+is_deeply($opt, {y => 'z'}, "Parsing was successful");
+
+
+
+# Try parsing a string
+
+$@ = '';
+$opt = eval {
+  XMLin(q(<opt name1="value1" name2="value2"></opt>));
+};
+
+my $expected = {
+                name1 => 'value1',
+                name2 => 'value2',
+              };
+
+is($@, '', "No error when parsing");
+is_deeply($opt, $expected, 'matches expectations (attributes)');
+
+
+# Try parsing a named external file
+
+$@ = '';
+$opt = eval{ XMLin($XMLFile); };
+is($@, '', "XML::Parser didn't choke on named external file");
+is_deeply($opt, {
+  location => 't/test1.xml'
+}, 'and contents parsed as expected');
+
+
+# Try parsing from an IO::Handle 
+
+$@ = '';
+my $fh = new IO::File;
+$XMLFile = File::Spec->catfile('t', '1_XMLin.xml');  # t/1_XMLin.xml
+eval {
+  $fh->open($XMLFile) || die "$!";
+  $opt = XMLin($fh);
+};
+is($@, '', "XML::Parser didn't choke on an IO::File object");
+is($opt->{location}, 't/1_XMLin.xml', 'and it parsed the right file');
+
+
+exit(0);
+
+sub warn_handler {
+  $last_warning = $_[0];
+}
diff --git a/t/B_Hooks.t b/t/B_Hooks.t
new file mode 100644 (file)
index 0000000..cde0b55
--- /dev/null
@@ -0,0 +1,136 @@
+# $Id: B_Hooks.t,v 1.2 2006/10/12 08:57:52 grantm Exp $
+# vim: syntax=perl
+
+use strict;
+use Test::More;
+use File::Spec;
+
+plan tests => 12;
+
+use_ok('XML::Simple');
+
+SKIP: {
+  eval { require Tie::IxHash };
+
+  skip "Tie::IxHash not installed", 3 if $@;
+
+  $@ = '';
+  eval <<'EOF';
+
+    package SimpleOrder;
+
+    use base qw(XML::Simple);
+    use Tie::IxHash;
+
+    sub new_hashref {
+      my $self = shift;
+      my %hash;
+      tie %hash, 'Tie::IxHash', @_;
+      return \%hash;
+    }
+EOF
+  ok(!$@, 'no errors processing SimpleOrder');
+
+  my $xs = SimpleOrder->new;
+  my $xml = q{
+    <nums>
+      <num id="one">I</num>
+      <num id="two">II</num>
+      <num id="three">III</num>
+      <num id="four">IV</num>
+      <num id="five">V</num>
+      <num id="six">VI</num>
+      <num id="seven">VII</num>
+    </nums>
+  };
+  my $expected = {
+    'one'   => { 'content' => 'I'   },
+    'two'   => { 'content' => 'II'  },
+    'three' => { 'content' => 'III' },
+    'four'  => { 'content' => 'IV'  },
+    'five'  => { 'content' => 'V'   },
+    'six'   => { 'content' => 'VI'  },
+    'seven' => { 'content' => 'VII' },
+  };
+
+  my $data = $xs->xml_in($xml);
+
+  is_deeply($data->{num}, $expected, 'hash content looks good');
+
+  is_deeply(
+    [ keys %{$data->{num}} ],
+    [ qw(one two three four five six seven) ],
+    'order of the hash keys looks good too'
+  );
+
+}
+
+
+my $xs = XML::Simple->new(cache => 'storable');
+my $sx = ElbarotsXS->new(cache => 'storable');
+
+isa_ok($sx, 'XML::Simple', 'object of class ElbarotsXS');
+
+my $src_file = File::Spec->catfile('t', 'test1.xml');
+
+is(
+  $xs->storable_filename($src_file), 
+  File::Spec->catfile('t', 'test1.stor'),
+  'default storable cache filename looks good'
+);
+
+my $cache_file = File::Spec->catfile('t', '1tset.stor'),;
+is(
+  $sx->storable_filename($src_file), 
+  $cache_file,
+  'overridden storable cache filename looks good'
+);
+
+SKIP: {
+  eval { require Storable };
+
+  skip "Storable not installed", 2 if $@;
+
+  unlink($cache_file),
+  ok(! -e $cache_file, 'overridden cache file does not exist before parse');
+  my $data = $sx->xml_in($src_file);
+  ok(-e $cache_file, 'overridden cache file does exist after parse');
+  unlink($cache_file),
+}
+
+my $data = eval {
+  $xs = XML::Simple->new(cache => 'floogle');
+  $xs->xml_in($src_file);
+};
+ok($@, 'bad cache scheme was rejected');
+
+$data = eval {
+  $sx = ElbarotsXS->new(cache => 'floogle');
+  $sx->xml_in($src_file);
+};
+ok(! $@, 'custom cache scheme was not rejected');
+is_deeply(
+  $data, 
+  { data => 'floogle' }, 
+  'custom cache reading method delivered the goods'
+);
+
+exit 0;
+
+
+package ElbarotsXS;
+
+use base 'XML::Simple';
+
+sub storable_filename {
+  my($self, $path) = @_;
+  
+  my($vol, $dir, $file) = File::Spec->splitpath( $path );
+  $file =~ s{\.xml$}{};
+
+  return File::Spec->catpath($vol, $dir, reverse($file) . '.stor');
+}
+
+sub cache_read_floogle {
+  return { data => 'floogle' };
+}
diff --git a/t/desertnet.src b/t/desertnet.src
new file mode 100644 (file)
index 0000000..b4148df
--- /dev/null
@@ -0,0 +1,13 @@
+<config>
+  <server name="sahara" osname="solaris" osversion="2.6">
+    <address>10.0.0.101</address>
+    <address>10.0.1.101</address>
+  </server>
+  <server name="gobi" osname="irix" osversion="6.5">
+    <address>10.0.0.102</address>
+  </server>
+  <server name="kalahari" osname="linux" osversion="2.0.34">
+    <address>10.0.0.103</address>
+    <address>10.0.1.103</address>
+  </server>
+</config>
diff --git a/t/lib/TagsToUpper.pm b/t/lib/TagsToUpper.pm
new file mode 100755 (executable)
index 0000000..48883f9
--- /dev/null
@@ -0,0 +1,38 @@
+package TagsToUpper;
+
+use XML::SAX::Base;
+
+use vars qw(@ISA);
+
+@ISA = ('XML::SAX::Base');
+
+sub start_element {
+  my $self    = shift;
+  my $element = shift;
+
+#  print Data::Dumper->Dump([$element], ['element']);
+  to_upper($element);
+  foreach (values(%{$element->{Attributes}})) { to_upper($_); }
+
+  $self->SUPER::start_element($element);
+}
+
+sub end_element {
+  my $self    = shift;
+  my $element = shift;
+
+  to_upper($element);
+
+  $self->SUPER::end_element($element);
+}
+
+sub to_upper {
+  my $ref = shift;
+
+  $ref->{LocalName} = uc($ref->{LocalName}) if($ref->{LocalName});
+  $ref->{Name}      = uc($ref->{Name})      if($ref->{LocalName});
+  $ref->{Prefix}    = uc($ref->{Prefix})    if($ref->{LocalName});
+}
+
+1;
+
diff --git a/t/srt.xml b/t/srt.xml
new file mode 100644 (file)
index 0000000..32e2b53
--- /dev/null
+++ b/t/srt.xml
@@ -0,0 +1,72 @@
+<?xml version='1.0' standalone='yes'?>\r
+\r
+<!--\r
+\r
+   This is an example of what a simple config file used by the System Release\r
+   Tool (SRT) might look like.  The file itself doesn't do anything other\r
+   than serve as a moderately complex test case for t/1_XMLin.t.\r
+\r
+   If you would like to find out more about the SRT, email the author at:\r
+\r
+     grantm@cpan.org\r
+\r
+-->\r
+   \r
+<opt>\r
+  <global tempdir="C:/Temp"\r
+         httpproxy="http://10.1.1.5:8080/"\r
+         proxyuser="foo"\r
+         proxypswd="bar" >\r
+\r
+    <exclude>/_vt</exclude>\r
+    <exclude>/save\b</exclude>\r
+    <exclude>\.bak$</exclude>\r
+    <exclude>\.\$\$\$$</exclude>\r
+\r
+  </global>\r
+\r
+  <pubpath name="test1" title="web_source -&gt; web_target1">\r
+    <source label="web_source"\r
+            root="C:/webshare/web_source" />\r
+    <target label="web_target1"\r
+            root="C:/webshare/web_target1"\r
+            temp="C:/webshare/web_target1/temp" />\r
+\r
+    <dir>wwwroot</dir>\r
+\r
+    <package name="images" dir="wwwroot/images" />\r
+\r
+  </pubpath>\r
+\r
+  <pubpath name="test2" title="web_source -&gt; web_target1 &amp; web_target2">\r
+    <source label="web_source"\r
+            root="C:/webshare/web_source" />\r
+    <target label="web_target1"\r
+            root="C:/webshare/web_target1"\r
+            temp="C:/webshare/web_target1/temp" />\r
+    <target label="web_target2"\r
+            root="C:/webshare/web_target2"\r
+            temp="C:/webshare/web_target2/temp" />\r
+\r
+    <dir>wwwroot</dir>\r
+\r
+    <package name="images" dir="wwwroot/images" />\r
+    <package name="templates" dir="wwwroot/templates" />\r
+    <package name="bios" dir="wwwroot/staff/bios" />\r
+\r
+  </pubpath>\r
+\r
+  <pubpath name="test3" title="web_source -&gt; web_target1 via HTTP">\r
+    <source label="web_source"\r
+            root="C:/webshare/web_source" />\r
+    <target label="web_target1"\r
+            root="http://127.0.0.1/cgi-bin/srt_slave.plx"\r
+            noproxy="1" />\r
+\r
+    <dir>wwwroot</dir>\r
+\r
+    <addexclude>\.pdf$</addexclude>\r
+\r
+  </pubpath>\r
+\r
+</opt>\r
diff --git a/t/subdir/test2.xml b/t/subdir/test2.xml
new file mode 100644 (file)
index 0000000..b5da6b4
--- /dev/null
@@ -0,0 +1 @@
+<opt location="t/subdir/test2.xml" />
diff --git a/t/test1.xml b/t/test1.xml
new file mode 100644 (file)
index 0000000..03787f9
--- /dev/null
@@ -0,0 +1 @@
+<opt location="t/test1.xml" />