Skip to content

Commit

Permalink
Merge pull request #6 from oat-sa/backport/xinclude
Browse files Browse the repository at this point in the history
Backport/xinclude
  • Loading branch information
llecaque committed Jun 29, 2015
2 parents 812faca + d6835b9 commit cd0ec2f
Show file tree
Hide file tree
Showing 13 changed files with 428 additions and 2 deletions.
106 changes: 106 additions & 0 deletions qtism/data/XInclude.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace qtism\data;

use qtism\data\rules\ResponseRule;
use qtism\data\rules\OutcomeRule;
use qtism\data\content\InlineStatic;
use qtism\data\content\FlowStatic;
use qtism\data\content\BlockStatic;
use qtism\common\utils\Format;
use \InvalidArgumentException;

/**
* From IMS QTI:
*
* Fragments are included using the Xinclude mechanism. (See [XINCLUDE].) The instance of include is treated as if
* it was actually an instance of the root element of the fragment referred to by the href attribute of the include
* element. For the purposes of this specification the xpointer mechanism defined by the XInclude specification must
* not be used. Also, all included fragments must be treated as parsed xml.
*
* This technique is similar to the inclusion of media objects (using object) but allows the inclusion of data that
* conforms to this specification, specifically, it allows the inclusion of interactions, static content, processing
* rules or, at test level whole sections, to be included from externally defined fragments.
*
* When including externally defined fragments the content of the fragment must satisfy the requirements of the
* specification in the context of the item in which it is being included. For example, interactions included
* from fragments must be correctly bound to response variables declared in the items.
*
* @author Jérôme Bogaerts <[email protected]>
*
*/
class XInclude extends ExternalQtiComponent implements BlockStatic, FlowStatic, InlineStatic, OutcomeRule, ResponseRule {

/**
* A base URI.
*
* @var string
* @qtism-bean-property
*/
private $xmlBase = '';

/**
* Create a new XInclude object.
*
* @param string $xmlString The XML Content of the node.
*/
public function __construct($xmlString) {
parent::__construct($xmlString);
}

/**
* Get the value of the xi:href attribute in the XML content.
*
* This is a convenience method.
*
* @return string
*/
public function getHref() {
$xml = $this->getXml();
return $xml->documentElement->getAttribute('href');
}

/**
* @see \qtism\data\content\Flow::setXmlBase()
*/
public function setXmlBase($xmlBase = '')
{
if (is_string($xmlBase) && (empty($xmlBase) || Format::isUri($xmlBase))) {
$this->xmlBase = $xmlBase;
} else {
$msg = "The 'xmlBase' argument must be an empty string or a valid URI, '" . $xmlBase . "' given";
throw new InvalidArgumentException($msg);
}
}

/**
* @see \qtism\data\content\Flow::getXmlBase()
*/
public function getXmlBase()
{
return $this->xmlBase;
}

/**
* @see \qtism\data\content\Flow::hasXmlBase()
*/
public function hasXmlBase()
{
return $this->getXmlBase() !== '';
}

/**
* @see \qtism\data\ExternalQtiComponent::getQtiClassName()
*/
public function getQtiClassName()
{
return 'include';
}

/**
* @see \qtism\data\ExternalQtiComponent::buildTargetNamespace()
*/
protected function buildTargetNamespace() {
$this->setTargetNamespace('http://www.w3.org/2001/XInclude');
}
}
1 change: 1 addition & 0 deletions qtism/data/content/AtomicInline.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace qtism\data\content;

use qtism\common\utils\Format;
use qtism\data\QtiComponentCollection;
use \InvalidArgumentException;

Expand Down
63 changes: 63 additions & 0 deletions qtism/data/storage/xml/XmlDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@

namespace qtism\data\storage\xml;

use qtism\common\utils\Url;
use qtism\data\QtiComponentCollection;
use qtism\data\QtiComponentIterator;
use qtism\data\QtiDocument;
use qtism\data\storage\xml\marshalling\MarshallerFactory;
use qtism\data\AssessmentTest;
use qtism\data\content\Flow;
use qtism\data\storage\xml\marshalling\Marshaller;
use qtism\data\storage\xml\marshalling\UnmarshallingException;
use qtism\data\QtiComponent;
use qtism\data\storage\Utils as StorageUtils;
use qtism\data\storage\xml\Utils as XmlUtils;
use \ReflectionClass;
use \DOMDocument;
use \DOMElement;
use \DOMException;
use \RuntimeException;
use \InvalidArgumentException;
use \LogicException;

/**
* This class represents a QTI-XML Document.
Expand Down Expand Up @@ -292,6 +298,63 @@ public function schemaValidate($filename = '') {
}
}


/**
* Resolve include components.
*
* After the item has been loaded using the load or loadFromString method,
* the include components can be resolved by calling this method. Files will
* be included following the rules described by the XInclude specification.
*
* @param boolean $validate Whether or not validate files being included.
* @throws \LogicException If the method is called prior the load or loadFromString method was called.
* @throws \qtism\data\storage\xml\XmlStorageException If an error occured while parsing or validating files to be included.
*/
public function xInclude($validate = false) {

if (($root = $this->getDocumentComponent()) !== false) {

$baseUri = str_replace('\\', '/', $this->getDomDocument()->documentElement->baseURI);
$pathinfo = pathinfo($baseUri);
$basePath = $pathinfo['dirname'];

$iterator = new QtiComponentIterator($root, array('include'));
foreach ($iterator as $include) {
$parent = $iterator->parent();

// Is the parent something we can deal with for replacement?
$reflection = new ReflectionClass($parent);

if ($reflection->hasMethod('getContent') === true && $parent->getContent() instanceof QtiComponentCollection) {
$href = $include->getHref();

if (Url::isRelative($href) === true) {
$href = Url::rtrim($basePath) . '/' . Url::ltrim($href);

$doc = new XmlDocument();
$doc->load($href, $validate);
$includeRoot = $doc->getDocumentComponent();

if ($includeRoot instanceof Flow) {
// Derive xml:base...
$xmlBase = Url::ltrim(str_replace($basePath, '', $href));
$xmlBasePathInfo = pathinfo($xmlBase);

if ($xmlBasePathInfo['dirname'] !== '.') {
$includeRoot->setXmlBase($xmlBasePathInfo['dirname'] . '/');
}
}

$parent->getContent()->replace($include, $includeRoot);
}
}
}
} else {
$msg = "Cannot include fragments prior to loading any file.";
throw new LogicException($msg);
}
}

/**
* Decorate the root element of the XmlAssessmentDocument with the appropriate
* namespaces and schema definition.
Expand Down
2 changes: 1 addition & 1 deletion qtism/data/storage/xml/marshalling/ContentMarshaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public function __construct() {
'selectPointInteraction', 'associableHotspot', 'hotspotChoice', 'graphicGapMatchInteraction',
'positionObjectInteraction', 'positionObjectStage', 'sliderInteraction', 'mediaInteraction',
'drawingInteraction', 'uploadInteraction', 'endAttemptInteraction', 'customInteraction',
'printedVariable', 'math');
'printedVariable', 'math', 'include');

private static $simpleComposites = array('a', 'abbr', 'acronym', 'b', 'big', 'cite', 'code', 'dfn', 'em', 'feedbackInline', 'templateInline', 'i',
'kbd', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'var', 'td', 'th', 'object',
Expand Down
2 changes: 1 addition & 1 deletion qtism/data/storage/xml/marshalling/ImgMarshaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected function marshall(QtiComponent $component) {
}

if ($component->hasXmlBase() === true) {
self::setXmlBase($element, $component->setXmlBase());
self::setXmlBase($element, $component->getXmlBase());
}

self::fillElement($element, $component);
Expand Down
1 change: 1 addition & 0 deletions qtism/data/storage/xml/marshalling/MarshallerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public function __construct() {
$this->addMappingEntry('templateBlock', 'qtism\\data\\storage\\xml\\marshalling\\TemplateElementMarshaller');
$this->addMappingEntry('hotspotChoice', 'qtism\\data\\storage\\xml\\marshalling\\HotspotMarshaller');
$this->addMappingEntry('associableHotspot', 'qtism\\data\\storage\\xml\\marshalling\\HotspotMarshaller');
$this->addMappingEntry('include', 'qtism\\data\\storage\\xml\\marshalling\\XIncludeMarshaller');
}

/**
Expand Down
70 changes: 70 additions & 0 deletions qtism/data/storage/xml/marshalling/XIncludeMarshaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2013-2015 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
* @author Jérôme Bogaerts <[email protected]>
* @license GPLv2
*/

namespace qtism\data\storage\xml\marshalling;

use qtism\data\XInclude;
use qtism\data\QtiComponent;
use \DOMElement;

/**
* Marshalling/Unmarshalling implementation for Include.
*
* @author Jérôme Bogaerts <[email protected]>
*
*/
class XIncludeMarshaller extends Marshaller
{
/**
* Marshall an XInclude object into a DOMElement object.
*
* @param \qtism\data\QtiComponent $component An XInclude object.
* @return \DOMElement The according DOMElement object.
* @throws \qtism\data\storage\marshalling\MarshallingException
*/
protected function marshall(QtiComponent $component)
{
return self::getDOMCradle()->importNode($component->getXml()->documentElement, true);
}

/**
* Unmarshall a DOMElement object corresponding to a math element.
*
* @param \DOMElement $element A DOMElement object.
* @return \qtism\data\QtiComponent A Math object.
* @throws \qtism\data\storage\marshalling\UnmarshallingException
*/
protected function unmarshall(DOMElement $element)
{
$node = $element->cloneNode(true);

return new XInclude($element->ownerDocument->saveXML($node));
}

/**
* @see \qtism\data\storage\xml\marshalling\Marshaller::getExpectedQtiClassName()
*/
public function getExpectedQtiClassName()
{
return 'include';
}
}
80 changes: 80 additions & 0 deletions test/qtism/data/storage/xml/XmlDocumentXIncludeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
namespace qtismtest\data\storage\xml;

use qtism\data\storage\xml\XmlDocument;
use qtism\data\storage\xml\XmlStorageException;

class XmlDocumentXIncludeTest extends \QtiSmTestCase {

public function testLoadAndSaveXIncludeNsInTag() {
$doc = new XmlDocument();
$doc->load(self::samplesDir() . 'custom/items/xinclude/xinclude_ns_in_tag.xml', true);

$includes = $doc->getDocumentComponent()->getComponentsByClassName('include');
$this->assertEquals(1, count($includes));
$this->assertEquals('xinclude_ns_in_tag_content1.xml', $includes[0]->getHref());

$file = tempnam('/tmp', 'qsm');
$doc->save($file);

// Does it validate again?
$doc = new XmlDocument();
try {
$doc->load($file, true);
$this->assertTrue(true);
} catch (XmlStorageException $e) {
$this->assertFalse(true, "The document using xinclude should validate after being saved.");
}
}

/**
* @depends testLoadAndSaveXIncludeNsInTag
*/
public function testLoadAndResolveXIncludeSameBase() {
$doc = new XmlDocument();
$doc->load(self::samplesDir() . 'custom/items/xinclude/xinclude_ns_in_tag.xml', true);

// At this moment, includes are not resolved.
$includes = $doc->getDocumentComponent()->getComponentsByClassName('include');
$this->assertEquals(1, count($includes));
// So no img components can be found...
$imgs = $doc->getDocumentComponent()->getComponentsByClassName('img');
$this->assertEquals(0, count($imgs));

$doc->xInclude();

// Now they are!
$includes = $doc->getDocumentComponent()->getComponentsByClassName('include');
$this->assertEquals(0, count($includes));

// And we should find an img component then!
$imgs = $doc->getDocumentComponent()->getComponentsByClassName('img');
$this->assertEquals(1, count($imgs));

// Check that xml:base was appropriately resolved. In this case,
// no content for xml:base because 'xinclude_ns_in_tag_content1.xml' is in the
// same directory as the main xml file.
$this->assertEquals('', $imgs[0]->getXmlBase());
}

/**
* @depends testLoadAndResolveXIncludeSameBase
*/
public function testLoadAndResolveXIncludeDifferentBase() {
$doc = new XmlDocument();
$doc->load(self::samplesDir() . 'custom/items/xinclude/xinclude_ns_in_tag_subfolder.xml', true);
$doc->xInclude();

$includes = $doc->getDocumentComponent()->getComponentsByClassName('include');
$this->assertEquals(0, count($includes));

// And we should find an img component then!
$imgs = $doc->getDocumentComponent()->getComponentsByClassName('img');
$this->assertEquals(1, count($imgs));

// Check that xml:base was appropriately resolved. In this case,
// no content for xml:base because 'xinclude_ns_in_tag_content1.xml' is in the
// same directory as the main xml file.
$this->assertEquals('subfolder/', $imgs[0]->getXmlBase());
}
}
Loading

0 comments on commit cd0ec2f

Please sign in to comment.