From a07a408f984f0efd9ed5cbe95833713739b1b1f1 Mon Sep 17 00:00:00 2001
From: Maciej Zgadzaj <maciej.zgadzaj@gmail.com>
Date: Tue, 18 Mar 2014 04:51:34 -0400
Subject: [PATCH] Issue #1961316 by TwoD, cruno, maciej.zgadzaj: Get data from all XML namespaces

---
 migrate_example/migrate_example.migrate.inc |   24 ++
 migrate_example/wine.inc                    |  343 +++++++++++++++++++++++++++
 migrate_example/xml/0002.xml                |    7 +
 migrate_example/xml/index2.xml              |    4 +
 migrate_example/xml/producers3.xml          |   17 ++
 migrate_example/xml/producers4.xml          |   17 ++
 plugins/sources/xml.inc                     |  129 ++++++++++-
 tests/plugins/sources/xml.test              |   65 +++++-
 8 files changed, 592 insertions(+), 14 deletions(-)
 create mode 100644 migrate_example/xml/0002.xml
 create mode 100644 migrate_example/xml/index2.xml
 create mode 100644 migrate_example/xml/producers3.xml
 create mode 100644 migrate_example/xml/producers4.xml

diff --git a/migrate_example/migrate_example.migrate.inc b/migrate_example/migrate_example.migrate.inc
index 772772b..8e24d23 100644
--- a/migrate_example/migrate_example.migrate.inc
+++ b/migrate_example/migrate_example.migrate.inc
@@ -148,6 +148,14 @@ function migrate_example_migrate_api() {
           'WineUser',
         ),
       ),
+      'WineProducerNamespaceXML' => array(
+        'class_name' => 'WineProducerNamespaceXMLMigration',
+        'group_name' => 'wine',
+        'dependencies' => array(
+          'WineRegion',
+          'WineUser',
+        ),
+      ),
       'WineProducerMultiXML' => array(
         'class_name' => 'WineProducerMultiXMLMigration',
         'group_name' => 'wine',
@@ -156,6 +164,14 @@ function migrate_example_migrate_api() {
           'WineUser',
         ),
       ),
+      'WineProducerMultiNamespaceXML' => array(
+        'class_name' => 'WineProducerMultiNamespaceXMLMigration',
+        'group_name' => 'wine',
+        'dependencies' => array(
+          'WineRegion',
+          'WineUser',
+        ),
+      ),
       'WineProducerXMLPull' => array(
         'class_name' => 'WineProducerXMLPullMigration',
         'group_name' => 'wine',
@@ -164,6 +180,14 @@ function migrate_example_migrate_api() {
           'WineUser',
         ),
       ),
+      'WineProducerNamespaceXMLPull' => array(
+        'class_name' => 'WineProducerNamespaceXMLPullMigration',
+        'group_name' => 'wine',
+        'dependencies' => array(
+          'WineRegion',
+          'WineUser',
+        ),
+      ),
       'WineWine' => array(
         'class_name' => 'WineWineMigration',
         'group_name' => 'wine',
diff --git a/migrate_example/wine.inc b/migrate_example/wine.inc
index 5688e2f..99c533b 100644
--- a/migrate_example/wine.inc
+++ b/migrate_example/wine.inc
@@ -587,6 +587,119 @@ class WineProducerXMLMigration extends XMLMigration {
 }
 
 /**
+ * TIP: An example of importing from an XML feed with namespaces.
+ * See the files in the xml directory - index2.xml contains a list of IDs
+ * to import, and <id>.xml is the data for a given producer.
+ *
+ * Note that, if basing a migration on an XML source, you need to derive it
+ * from XMLMigration instead of Migration.
+ */
+class WineProducerNamespaceXMLMigration extends XMLMigration {
+  public function __construct($arguments) {
+    parent::__construct($arguments);
+    $this->description = t('Namespaced XML feed of wine producers of the world');
+
+    // There isn't a consistent way to automatically identify appropriate
+    // "fields" from an XML feed, so we pass an explicit list of source fields.
+    $fields = array(
+      'pr:name' => t('Producer name'),
+      'pr:description' => t('Description of producer'),
+      'pr:authorid' => t('Numeric ID of the author'),
+      'pr:region' => t('Name of region'),
+    );
+
+    // IMPORTANT: Do not try this at home! We have included importable files
+    // with the migrate_example module so it can be very simply installed and
+    // run, but you should never include any data you want to keep private
+    // (especially user data like email addresses, phone numbers, etc.) in the
+    // module directory. Your source data should be outside of the webroot, and
+    // should not be anywhere where it may get committed into a revision control
+    // system.
+
+    // This can also be an URL instead of a file path.
+    $xml_folder = DRUPAL_ROOT . '/' .
+                  drupal_get_path('module', 'migrate_example') . '/xml/';
+    $list_url = $xml_folder . 'index2.xml';
+    // Each ID retrieved from the list URL will be plugged into :id in the
+    // item URL to fetch the specific objects.
+    $item_url = $xml_folder . ':id.xml';
+
+    // We use the MigrateSourceList class for any source where we obtain the
+    // list of IDs to process separately from the data for each item. The
+    // listing and item are represented by separate classes, so for example we
+    // could replace the XML listing with a file directory listing, or the XML
+    // item with a JSON item.
+    $list = new MigrateListXML($list_url, array('wn' => 'http://www.wine.org/wine'));
+    $item = new MigrateItemXML($item_url, array('pr' => 'http://www.wine.org/wine-producers'));
+    $this->source = new MigrateSourceList($list, $item, $fields);
+
+    $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+    // The source ID here is the one retrieved from the XML listing file, and
+    // used to identify the specific item's file
+    $this->map = new MigrateSQLMap($this->machineName,
+      array(
+        'sourceid' => array(
+          'type' => 'varchar',
+          'length' => 4,
+          'not null' => TRUE,
+        )
+      ),
+      MigrateDestinationNode::getKeySchema()
+    );
+
+    // TIP: Note that for XML sources, in addition to the source field passed to
+    // addFieldMapping (the name under which it will be saved in the data row
+    // passed through the migration process) we specify the Xpath used to
+    // retrieve the value from the XML.
+    $this->addFieldMapping('title', 'pr:name')
+         ->xpath('/pr:producer/pr:name');
+    $this->addFieldMapping('uid', 'pr:authorid')
+         ->xpath('/pr:producer/pr:authorid')
+         ->sourceMigration('WineUser')
+         ->defaultValue(1);
+    $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+         ->xpath('/pr:producer/pr:region');
+    $this->addFieldMapping('body', 'pr:description')
+         ->xpath('/pr:producer/pr:description');
+
+    $this->addUnmigratedDestinations(array(
+        'body:summary', 'body:format', 'body:language',
+      'changed',
+      'comment',
+      'created',
+      'is_new',
+      'language',
+      'log',
+        'migrate_example_wine_regions:create_term',
+        'migrate_example_wine_regions:ignore_case',
+        'migrate_example_wine_regions:source_type',
+      'promote',
+      'revision',
+      'revision_uid',
+      'status',
+      'sticky',
+      'tnid',
+      'translate',
+    ));
+
+    $destination_fields = $this->destination->fields();
+    if (isset($destination_fields['path'])) {
+      $this->addFieldMapping('path')
+           ->issueGroup(t('DNM'));
+      if (isset($destination_fields['pathauto'])) {
+        $this->addFieldMapping('pathauto')
+             ->issueGroup(t('DNM'));
+      }
+    }
+    if (module_exists('statistics')) {
+      $this->addUnmigratedDestinations(
+        array('totalcount', 'daycount', 'timestamp'));
+    }
+  }
+}
+
+/**
  * TIP: An example of importing from an XML feed where both the id and the
  * data to import are in the same file.  The id is a part of the data.  See
  * the file in the xml directory - producers.xml which contains all IDs and
@@ -708,6 +821,129 @@ class WineProducerMultiXMLMigration extends XMLMigration {
 }
 
 /**
+ * TIP: An example of importing from an XML feed with namespaces, where both
+ * the id and the data to import are in the same file.  The id is a part of
+ * the data. See the file in the xml directory - producers3.xml which contains
+ * all IDs and producer data for this example.
+ *
+ * Note that, if basing a migration on an XML source, you need to derive it
+ * from XMLMigration instead of Migration.
+ */
+class WineProducerMultiNamespaceXMLMigration extends XMLMigration {
+  public function __construct($arguments) {
+    parent::__construct($arguments);
+    $this->description =
+      t('Namespaced XML feed (multi items) of wine producers of the world');
+
+    // There isn't a consistent way to automatically identify appropriate
+    // "fields" from an XML feed, so we pass an explicit list of source fields.
+    $fields = array(
+      'pr:name' => t('Producer name'),
+      'pr:description' => t('Description of producer'),
+      'pr:authorid' => t('Numeric ID of the author'),
+      'pr:region' => t('Name of region'),
+    );
+
+    // IMPORTANT: Do not try this at home! We have included importable files
+    // with the migrate_example module so it can be very simply installed and
+    // run, but you should never include any data you want to keep private
+    // (especially user data like email addresses, phone numbers, etc.) in the
+    // module directory. Your source data should be outside of the webroot, and
+    // should not be anywhere where it may get committed into a revision control
+    // system.
+
+    // This can also be an URL instead of a file path.
+    $xml_folder = DRUPAL_ROOT . '/' . drupal_get_path('module', 'migrate_example') . '/xml/';
+    $items_url = $xml_folder . 'producers3.xml';
+
+    // We use the MigrateSourceMultiItems class for any source where we obtain
+    // the list of IDs to process and the data for each item from the same
+    // file. Examples include multiple items defined in a single xml file or a
+    // single json file where in both cases the id is part of the item.
+
+    // This is the xpath identifying the items to be migrated, relative to the
+    // document.
+    $item_xpath = '/pr:producers/pr:producer';
+    // This is the xpath relative to the individual items - thus the full xpath
+    // of an ID will be /producers/producer/sourceid.
+    $item_ID_xpath = 'pr:sourceid';
+    // All XML namespaces used in the XML file need to be defined here too.
+    $namespaces = array('pr' => 'http://www.wine.org/wine-producers');
+
+    $items_class = new MigrateItemsXML($items_url, $item_xpath, $item_ID_xpath, $namespaces);
+    $this->source = new MigrateSourceMultiItems($items_class, $fields);
+
+    $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+    // The source ID here is the one retrieved from each data item in the XML
+    // file, and used to identify specific items
+    $this->map = new MigrateSQLMap($this->machineName,
+      array(
+        'sourceid' => array(
+          'type' => 'varchar',
+          'length' => 4,
+          'not null' => TRUE,
+        )
+      ),
+      MigrateDestinationNode::getKeySchema()
+    );
+
+    // TIP: Note that for XML sources, in addition to the source field passed to
+    // addFieldMapping (the name under which it will be saved in the data row
+    // passed through the migration process) we specify the Xpath used to
+    // retrieve the value from the XML.
+    // TIP: Note that all xpaths for fields begin at the last element of the
+    // item xpath since each item xml chunk is processed individually.
+    // (ex. xpath=name is equivalent to a full xpath of
+    // /producers/producer/name).
+    $this->addFieldMapping('title', 'pr:name')
+         ->xpath('pr:name');
+    $this->addFieldMapping('uid', 'pr:authorid')
+         ->xpath('pr:authorid')
+         ->sourceMigration('WineUser')
+         ->defaultValue(1);
+    $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+         ->xpath('pr:region');
+    $this->addFieldMapping('body', 'pr:description')
+         ->xpath('pr:description');
+
+    $this->addUnmigratedDestinations(array(
+        'body:summary', 'body:format', 'body:language',
+      'changed',
+      'comment',
+      'created',
+      'is_new',
+      'language',
+      'log',
+        'migrate_example_wine_regions:create_term',
+        'migrate_example_wine_regions:ignore_case',
+        'migrate_example_wine_regions:source_type',
+      'promote',
+      'revision',
+      'revision_uid',
+      'status',
+      'sticky',
+      'tnid',
+      'translate',
+    ));
+
+    $destination_fields = $this->destination->fields();
+    if (isset($destination_fields['path'])) {
+      $this->addFieldMapping('path')
+           ->issueGroup(t('DNM'));
+      if (isset($destination_fields['pathauto'])) {
+        $this->addFieldMapping('pathauto')
+             ->issueGroup(t('DNM'));
+      }
+    }
+    if (module_exists('statistics')) {
+      $this->addUnmigratedDestinations(
+        array('totalcount', 'daycount', 'timestamp'));
+    }
+  }
+}
+
+/**
  * TIP: An alternative approach using MigrateSourceSQL. This uses a different
  * XML library, which advances element-by-element through the XML file rather
  * than reading in the whole file. This source will work better with large XML
@@ -812,6 +1048,113 @@ class WineProducerXMLPullMigration extends XMLMigration {
   }
 }
 
+/**
+ * TIP: An alternative approach using MigrateSourceSQL. This uses a different
+ * XML library, which advances element-by-element through the XML file rather
+ * than reading in the whole file. This source will work better with large XML
+ * files, but is slower for small files and has a more restrictive query
+ * language for selecting the elements to process.
+ */
+class WineProducerNamespaceXMLPullMigration extends XMLMigration {
+  public function __construct($arguments) {
+    parent::__construct($arguments);
+    $this->description = t('XML feed with namespaces (pull) of wine producers of the world');
+
+    $fields = array(
+      'pr:name' => t('Producer name'),
+      'pr:description' => t('Description of producer'),
+      'pr:authorid' => t('Numeric ID of the author'),
+      'pr:region' => t('Name of region'),
+    );
+
+    // IMPORTANT: Do not try this at home! We have included importable files
+    // with the migrate_example module so it can be very simply installed and
+    // run, but you should never include any data you want to keep private
+    // (especially user data like email addresses, phone numbers, etc.) in the
+    // module directory. Your source data should be outside of the webroot, and
+    // should not be anywhere where it may get committed into a revision control
+    // system.
+
+    // This can also be an URL instead of a local file path.
+    $xml_folder = DRUPAL_ROOT . '/' .
+                  drupal_get_path('module', 'migrate_example') . '/xml/';
+    $items_url = $xml_folder . 'producers4.xml';
+
+    // As with MigrateSourceMultiItems, this applies where there is not a
+    // separate list of IDs to process - the source XML file is entirely
+    // self-contained. For the ID path, and xpath for each component, we can
+    // use the full xpath syntax as usual. However, the syntax to select the
+    // elements that correspond to objects to import is more limited. It must
+    // be a fully-qualified path to the element (i.e.,
+    // /producers/producer rather than just //producer).
+    $item_xpath = '/pr:producers/pr:producer';  // relative to document
+    $item_ID_xpath = 'pr:sourceid';          // relative to item_xpath
+    $namespaces = array('pr' => 'http://www.wine.org/wine-producers');
+
+    $this->source = new MigrateSourceXML($items_url, $item_xpath,
+                                         $item_ID_xpath, $fields,
+                                         array(), $namespaces);
+
+    $this->destination = new MigrateDestinationNode('migrate_example_producer');
+
+    $this->map = new MigrateSQLMap($this->machineName,
+      array(
+        'sourceid' => array(
+          'type' => 'varchar',
+          'length' => 4,
+          'not null' => TRUE,
+        )
+      ),
+      MigrateDestinationNode::getKeySchema()
+    );
+
+    $this->addFieldMapping('title', 'pr:name')
+         ->xpath('pr:name');
+    $this->addFieldMapping('uid', 'pr:authorid')
+         ->xpath('pr:authorid')
+         ->sourceMigration('WineUser')
+         ->defaultValue(1);
+    $this->addFieldMapping('migrate_example_wine_regions', 'pr:region')
+         ->xpath('pr:region');
+    $this->addFieldMapping('body', 'pr:description')
+         ->xpath('pr:description');
+
+    $this->addUnmigratedDestinations(array(
+        'body:summary', 'body:format', 'body:language',
+      'changed',
+      'comment',
+      'created',
+      'is_new',
+      'language',
+      'log',
+        'migrate_example_wine_regions:create_term',
+        'migrate_example_wine_regions:ignore_case',
+        'migrate_example_wine_regions:source_type',
+      'promote',
+      'revision',
+      'revision_uid',
+      'status',
+      'sticky',
+      'tnid',
+      'translate',
+    ));
+
+    $destination_fields = $this->destination->fields();
+    if (isset($destination_fields['path'])) {
+      $this->addFieldMapping('path')
+           ->issueGroup(t('DNM'));
+      if (isset($destination_fields['pathauto'])) {
+        $this->addFieldMapping('pathauto')
+             ->issueGroup(t('DNM'));
+      }
+    }
+    if (module_exists('statistics')) {
+      $this->addUnmigratedDestinations(
+        array('totalcount', 'daycount', 'timestamp'));
+    }
+  }
+}
+
 // TODO: Add node_reference field pointing to producer
 class WineWineMigration extends AdvancedExampleMigration {
   public function __construct($arguments) {
diff --git a/migrate_example/xml/0002.xml b/migrate_example/xml/0002.xml
new file mode 100644
index 0000000..380113b
--- /dev/null
+++ b/migrate_example/xml/0002.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<pr:producer xmlns:pr="http://www.wine.org/wine-producers">
+  <pr:name>Château Latour</pr:name>
+  <pr:description>Makers of grand vin Chateau Latour, Les Forts de Latour and Pauillac</pr:description>
+  <pr:authorid>3</pr:authorid>
+  <pr:region>Bordeaux</pr:region>
+</pr:producer>
diff --git a/migrate_example/xml/index2.xml b/migrate_example/xml/index2.xml
new file mode 100644
index 0000000..9dfa500
--- /dev/null
+++ b/migrate_example/xml/index2.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<wn:content xmlns:wn="http://www.wine.org/wine">
+  <wn:sourceid>0002</wn:sourceid>
+</wn:content>
diff --git a/migrate_example/xml/producers3.xml b/migrate_example/xml/producers3.xml
new file mode 100644
index 0000000..62dabdd
--- /dev/null
+++ b/migrate_example/xml/producers3.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<pr:producers xmlns:pr="http://www.wine.org/wine-producers">
+  <pr:producer>
+    <pr:sourceid>0009</pr:sourceid>
+    <pr:name>Château Feytit Clinet</pr:name>
+    <pr:description>Good things, they say, come in small packages; this is certainly the case at Château Feyit Clinet, the maker of thelabel Pomerol.</pr:description>
+    <pr:authorid>1</pr:authorid>
+    <pr:region>Pomerol</pr:region>
+  </pr:producer>
+  <pr:producer>
+    <pr:sourceid>0010</pr:sourceid>
+    <pr:name>Château Doisy-Védrines</pr:name>
+    <pr:description>Blessed with ancient vines, the fruit here is often subject to high levels of Botrytis (or noble rot), which shrinks the grapes and concentrates sugar levels in the remaining juice.</pr:description>
+    <pr:authorid>3</pr:authorid>
+    <pr:region>Barsac</pr:region>
+  </pr:producer>
+</pr:producers>
diff --git a/migrate_example/xml/producers4.xml b/migrate_example/xml/producers4.xml
new file mode 100644
index 0000000..aaeeca3
--- /dev/null
+++ b/migrate_example/xml/producers4.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<pr:producers xmlns:pr="http://www.wine.org/wine-producers">
+  <pr:producer>
+    <pr:sourceid>0009</pr:sourceid>
+    <pr:name>Château Bourgneuf</pr:name>
+    <pr:description>Showing attractive red and dark fruits, cherry and plum, it is rich and even slightly racy, before concluding with classic Pomerol concentration and focus.</pr:description>
+    <pr:authorid>3</pr:authorid>
+    <pr:region>Pomerol</pr:region>
+  </pr:producer>
+  <pr:producer>
+    <pr:sourceid>0010</pr:sourceid>
+    <pr:name>Château Doisy-Daëne</pr:name>
+    <pr:description>Medium bodied, with elegance rather than density there is a wide spectrum of flavours; apples, peaches, lemongrass and a touch of white spice. Delicious now.</pr:description>
+    <pr:authorid>9</pr:authorid>
+    <pr:region>Barsac</pr:region>
+  </pr:producer>
+</pr:producers>
diff --git a/plugins/sources/xml.inc b/plugins/sources/xml.inc
index 7b0f3cf..3ab3ffc 100644
--- a/plugins/sources/xml.inc
+++ b/plugins/sources/xml.inc
@@ -33,11 +33,19 @@ class MigrateListXML extends MigrateList {
   protected $listUrl;
 
   /**
+   * An array of namespaces to explicitly register before Xpath queries.
+   *
+   * @var array
+   */
+  protected $namespaces;
+
+  /**
    * {@inheritdoc}
    */
-  public function __construct($list_url) {
+  public function __construct($list_url, array $namespaces = array()) {
     parent::__construct();
     $this->listUrl = $list_url;
+    $this->namespaces = $namespaces;
     // Suppress errors during parsing, so we can pick them up after.
     libxml_use_internal_errors(TRUE);
   }
@@ -62,6 +70,7 @@ class MigrateListXML extends MigrateList {
     $xml = simplexml_load_file($this->listUrl);
     migrate_instrument_stop("Retrieve $this->listUrl");
     if ($xml !== FALSE) {
+      $this->registerNamespaces($xml);
       return $this->getIDsFromXML($xml);
     }
     else {
@@ -95,6 +104,15 @@ class MigrateListXML extends MigrateList {
     foreach ($xml as $element) {
       $ids[] = (string) $element;
     }
+    // Additionally, if there are any namespaces registered, try to parse
+    // elements with namespaces as well.
+    if ($namespaces = $xml->getNamespaces()) {
+      foreach ($namespaces as $prefix => $url) {
+        foreach ($xml->children($url) as $element) {
+          $ids[] = (string) $element;
+        }
+      }
+    }
     return array_unique($ids);
   }
 
@@ -108,10 +126,30 @@ class MigrateListXML extends MigrateList {
    */
   public function computeCount() {
     $xml = simplexml_load_file($this->listUrl);
+    $this->registerNamespaces($xml);
     // Number of sourceid elements beneath the top-level element.
     $count = count($xml);
+    // Additionally, if there are any namespaces registered, try to count
+    // elements with namespaces as well.
+    if ($namespaces = $xml->getNamespaces()) {
+      foreach ($namespaces as $prefix => $url) {
+        $count += count($xml->children($url));
+      }
+    }
     return $count;
   }
+
+  /**
+   * Explicitly register namespaces on an XML element.
+   *
+   * @param SimpleXMLElement $xml
+   *   A SimpleXMLElement to register the namespaces on.
+   */
+  protected function registerNamespaces(SimpleXMLElement &$xml) {
+    foreach ($this->namespaces as $prefix => $namespace) {
+      $xml->registerXPathNamespace($prefix, $namespace);
+    }
+  }
 }
 
 /**
@@ -128,16 +166,36 @@ class MigrateItemXML extends MigrateItem {
   protected $itemUrl;
 
   /**
+   * An array of namespaces to explicitly register before Xpath queries.
+   *
+   * @var array
+   */
+  protected $namespaces;
+
+  /**
    * {@inheritdoc}
    */
-  public function __construct($item_url) {
+  public function __construct($item_url, array $namespaces = array()) {
     parent::__construct();
     $this->itemUrl = $item_url;
+    $this->namespaces = $namespaces;
     // Suppress errors during parsing, so we can pick them up after.
     libxml_use_internal_errors(TRUE);
   }
 
   /**
+   * Explicitly register namespaces on an XML element.
+   *
+   * @param SimpleXMLElement $xml
+   *   A SimpleXMLElement to register the namespaces on.
+   */
+  protected function registerNamespaces(SimpleXMLElement &$xml) {
+    foreach ($this->namespaces as $prefix => $namespace) {
+      $xml->registerXPathNamespace($prefix, $namespace);
+    }
+  }
+
+  /**
    * {@inheritdoc}
    *
    * Implementors are expected to return an object representing a source item.
@@ -155,6 +213,7 @@ class MigrateItemXML extends MigrateItem {
     // Get the XML object at the specified URL.
     $xml = $this->loadXmlUrl($item_url);
     if ($xml !== FALSE) {
+      $this->registerNamespaces($xml);
       $return = new stdclass();
       $return->xml = $xml;
       return $return;
@@ -375,6 +434,13 @@ class MigrateItemsXML extends MigrateItems {
   protected $currentUrl;
 
   /**
+   * An array of namespaces to explicitly register before Xpath queries.
+   *
+   * @var array
+   */
+  protected $namespaces;
+
+  /**
    * Stores the loaded XML document from currentUrl.
    *
    * @var SimpleXMLElement
@@ -433,7 +499,8 @@ class MigrateItemsXML extends MigrateItems {
   /**
    * {@inheritdoc}
    */
-  public function __construct($urls, $item_xpath = 'item', $item_id_xpath = 'id') {
+  public function __construct($urls, $item_xpath = 'item', $item_id_xpath = 'id',
+                              array $namespaces = array()) {
     parent::__construct();
     if (!is_array($urls)) {
       $urls = array($urls);
@@ -441,12 +508,25 @@ class MigrateItemsXML extends MigrateItems {
     $this->urls = $urls;
     $this->itemXpath = $item_xpath;
     $this->itemIDXpath = $item_id_xpath;
+    $this->namespaces = $namespaces;
 
     // Suppress errors during parsing, so we can pick them up after.
     libxml_use_internal_errors(TRUE);
   }
 
   /**
+   * Explicitly register namespaces on an XML element.
+   *
+   * @param SimpleXMLElement $xml
+   *   A SimpleXMLElement to register the namespaces on.
+   */
+  protected function registerNamespaces(SimpleXMLElement &$xml) {
+    foreach ($this->namespaces as $prefix => $namespace) {
+      $xml->registerXPathNamespace($prefix, $namespace);
+    }
+  }
+
+  /**
    * Our public face is the URL list we're getting items from.
    */
   public function __toString() {
@@ -470,7 +550,7 @@ class MigrateItemsXML extends MigrateItems {
   public function &xml() {
     if (!empty($this->currentUrl)) {
       $this->currentXml = simplexml_load_file($this->currentUrl);
-      if (!$this->currentXml) {
+      if ($this->currentXml === FALSE) {
         Migration::displayMessage(t(
           'Loading of !currentUrl failed:',
           array('!currentUrl' => $this->currentUrl)
@@ -479,6 +559,9 @@ class MigrateItemsXML extends MigrateItems {
           Migration::displayMessage(self::parseLibXMLError($error));
         }
       }
+      else {
+        $this->registerNamespaces($this->currentXml);
+      }
     }
     return $this->currentXml;
   }
@@ -576,6 +659,11 @@ class MigrateItemsXML extends MigrateItems {
     $ids = array();
     if ($result) {
       foreach ($result as $element) {
+        if (!isset($element)) {
+          continue;
+        }
+        // Namespaces must be reapplied after xpath().
+        $this->registerNamespaces($element);
         $id = $this->getItemID($element);
         if (!is_null($id)) {
           $ids[] = (string) $id;
@@ -646,6 +734,11 @@ class MigrateItemsXML extends MigrateItems {
 
     if ($result) {
       foreach ($result as $item_xml) {
+        if (!isset($item_xml)) {
+          continue;
+        }
+        // Namespaces must be reapplied after xpath().
+        $this->registerNamespaces($item_xml);
         $id = $this->getItemID($item_xml);
         $item = new stdclass();
         $item->xml = $item_xml;
@@ -685,7 +778,7 @@ class MigrateItemsXML extends MigrateItems {
    */
   protected function getElementValue($item_xml, $xpath) {
     $value = NULL;
-    if ($item_xml) {
+    if ($item_xml->asXML()) {
       $result = $item_xml->xpath($xpath);
       if ($result) {
         $value = (string) $result[0];
@@ -1006,7 +1099,7 @@ class MigrateXMLReader implements Iterator {
    *   Indicates if current element is valid
    */
   public function valid() {
-    return !empty($this->currentElement);
+    return $this->currentElement instanceof SimpleXMLElement;
   }
 }
 
@@ -1045,6 +1138,13 @@ class MigrateSourceXML extends MigrateSource {
   protected $activeUrl = NULL;
 
   /**
+   * An array of namespaces to explicitly register before Xpath queries.
+   *
+   * @var array
+   */
+  protected $namespaces;
+
+  /**
    * Store the query string used to recognize elements being iterated
    * so we can create reader objects on the fly.
    *
@@ -1097,7 +1197,7 @@ class MigrateSourceXML extends MigrateSource {
    *     MigrateXMLReader).
    */
   public function __construct($urls, $element_query, $id_query, array $fields = array(),
-                              array $options = array()) {
+                              array $options = array(), array $namespaces = array()) {
     parent::__construct($options);
     if (empty($options['reader_class'])) {
       $reader_class = 'MigrateXMLReader';
@@ -1116,6 +1216,19 @@ class MigrateSourceXML extends MigrateSource {
     $this->idQuery = $id_query;
     $this->readerClass = $reader_class;
     $this->fields = $fields;
+    $this->namespaces = $namespaces;
+  }
+
+  /**
+   * Explicitly register namespaces on an XML element.
+   *
+   * @param SimpleXMLElement $xml
+   *   A SimpleXMLElement to register the namespaces on.
+   */
+  protected function registerNamespaces(SimpleXMLElement &$xml) {
+    foreach ($this->namespaces as $prefix => $namespace) {
+      $xml->registerXPathNamespace($prefix, $namespace);
+    }
   }
 
   /**
@@ -1209,6 +1322,7 @@ class MigrateSourceXML extends MigrateSource {
       $row = new stdClass();
       $row->$key_name = $this->reader->key();
       $row->xml = $this->reader->current();
+      $this->registerNamespaces($row->xml);
     }
     else {
       // The current source is at the end, try to load the next source.
@@ -1216,6 +1330,7 @@ class MigrateSourceXML extends MigrateSource {
         $row = new stdClass();
         $row->$key_name = $this->reader->key();
         $row->xml = $this->reader->current();
+        $this->registerNamespaces($row->xml);
       }
     }
 
diff --git a/tests/plugins/sources/xml.test b/tests/plugins/sources/xml.test
index 9a17e29..1b4eeff 100644
--- a/tests/plugins/sources/xml.test
+++ b/tests/plugins/sources/xml.test
@@ -29,22 +29,51 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
     $result = $migration->processImport();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
       t('Region term import returned RESULT_COMPLETED'));
+
     $migration = Migration::getInstance('WineFileCopy');
     $result = $migration->processImport();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
       t('File import returned RESULT_COMPLETED'));
+
     $migration = Migration::getInstance('WineRole');
     $result = $migration->processImport();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
       t('Role import returned RESULT_COMPLETED'));
+
     $migration = Migration::getInstance('WineUser');
     $result = $migration->processImport();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
       t('User import returned RESULT_COMPLETED'));
-    $migration = Migration::getInstance('WineProducerXML');
-    $result = $migration->processImport();
+
+    $migration1 = Migration::getInstance('WineProducerXML');
+    $result = $migration1->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node import 1 returned RESULT_COMPLETED'));
+
+    $migration2 = Migration::getInstance('WineProducerNamespaceXML');
+    $result = $migration2->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node import 2 returned RESULT_COMPLETED'));
+
+    $migration3 = Migration::getInstance('WineProducerMultiXML');
+    $result = $migration3->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node import 3 returned RESULT_COMPLETED'));
+
+    $migration4 = Migration::getInstance('WineProducerMultiNamespaceXML');
+    $result = $migration4->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node import 4 returned RESULT_COMPLETED'));
+
+    $migration5 = Migration::getInstance('WineProducerXMLPull');
+    $result = $migration5->processImport();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node import 5 returned RESULT_COMPLETED'));
+
+    $migration6 = Migration::getInstance('WineProducerNamespaceXMLPull');
+    $result = $migration6->processImport();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
-      t('Producer node import returned RESULT_COMPLETED'));
+      t('Producer node import 6 returned RESULT_COMPLETED'));
 
     // Gather producer nodes, and their corresponding input data
     $rawnodes = node_load_multiple(FALSE, array('type' => 'migrate_example_producer'), TRUE);
@@ -54,7 +83,7 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
       $producer_nodes[$node->title] = $node;
     }
 
-    $this->assertEqual(count($producer_nodes), 1,
+    $this->assertEqual(count($producer_nodes), 10,
       t('Counts of producer nodes and input rows match'));
 
     // Test each base node field
@@ -81,10 +110,32 @@ class MigrateXMLUnitTest extends DrupalWebTestCase {
     $this->assertEqual($region[0]['tid'], $term->tid,
       t('region properly migrated'));
 
-    // Test rollback
-    $result = $migration->processRollback();
+    // Rollback producer migrations
+    $result = $migration1->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node rollback 1 returned RESULT_COMPLETED'));
+
+    $result = $migration2->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node rollback 2 returned RESULT_COMPLETED'));
+
+    $result = $migration3->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node rollback 3 returned RESULT_COMPLETED'));
+
+    $result = $migration4->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node rollback 4 returned RESULT_COMPLETED'));
+
+    $result = $migration5->processRollback();
     $this->assertEqual($result, Migration::RESULT_COMPLETED,
-      t('Producer node rollback returned RESULT_COMPLETED'));
+      t('Producer node rollback 5 returned RESULT_COMPLETED'));
+
+    $result = $migration6->processRollback();
+    $this->assertEqual($result, Migration::RESULT_COMPLETED,
+      t('Producer node rollback 6 returned RESULT_COMPLETED'));
+
+    // Test rollback
     $rawnodes = node_load_multiple(FALSE, array('type' => 'migrate_example_producer'), TRUE);
     $this->assertEqual(count($rawnodes), 0, t('All nodes deleted'));
     $count = db_select('migrate_map_wineproducerxml', 'map')
-- 
1.7.2.5

