diff --git a/src/SchemaReader.php b/src/SchemaReader.php index a1bf4846..42c06a4d 100644 --- a/src/SchemaReader.php +++ b/src/SchemaReader.php @@ -277,6 +277,9 @@ function ( case 'import': $callback = $this->loadImport($schema, $childNode); break; + case 'redefine': + $callback = $this->loadRedefine($schema, $childNode); + break; case 'element': $callback = $this->loadElementDef($schema, $childNode); break; @@ -1170,6 +1173,32 @@ private function loadImport( return $this->loadImportFresh($namespace, $schema, $file); } + private function loadRedefine(Schema $schema, DOMElement $node): Closure + { + $base = urldecode($node->ownerDocument->documentURI); + $file = UrlUtils::resolveRelativeUrl($base, $node->getAttribute('schemaLocation')); + + if (isset($this->loadedFiles[$file])) { + /* @var $redefined Schema */ + $redefined = clone $this->loadedFiles[$file]; + + if ($schema->getTargetNamespace() !== $redefined->getTargetNamespace()) { + $redefined->setTargetNamespace($schema->getTargetNamespace()); + } + + $schema->addSchema($redefined); + + return function () use ($redefined, $node, $schema): void { + $callbacks = $this->schemaNode($redefined, $node, $schema); + foreach ($callbacks as $callback) { + $callback(); + } + }; + } + + return $this->loadImportFresh((string) $schema->getTargetNamespace(), $schema, $file); + } + private function createOrUseSchemaForNs( Schema $schema, string $namespace diff --git a/tests/RedefineTest.php b/tests/RedefineTest.php new file mode 100644 index 00000000..d2782555 --- /dev/null +++ b/tests/RedefineTest.php @@ -0,0 +1,145 @@ +reader->readString( + ' + + + + + + + + + + ', 'http://www.example.com/xsd.xsd'); + + $schema = $this->reader->readString( + ' + + + + + + + + + + + + + + + '); + + // check if schema is not included + // we don't want to redefine original schema + $this->assertNotContains($remoteSchema, $schema->getSchemas(), '', true); + + /* @var $localAttr \GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef */ + + // it should inherit namespace of main schema + $localAttr = $schema->findElement('addressee', 'http://www.user.com'); + $this->assertNotNull($localAttr); + + // find author element + $localAttr = $schema->findElement('author', 'http://www.user.com'); + $this->assertNotNull($localAttr); + + /* @var $type \GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType */ + $type = $localAttr->getType(); + + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type); + + $children = array(); + foreach ($type->getElements() as $element) { + $children[] = $element->getName(); + } + + $this->assertContains('generation', $children); + } + + public function testReadSchemaLocation() + { + $schema = $this->reader->readFile(__DIR__.'/schema/extend-components.xsd'); + $this->assertInstanceOf(Schema::class, $schema); + + $this->assertEquals('spec:example:xsd:CommonBasicComponents-1.0', $schema->getTargetNamespace()); + + // defined in /schema/base-components.xsd + $dateElement = $schema->findElement('Date', 'spec:example:xsd:CommonBasicComponents-1.0'); + $this->assertNotNull($dateElement); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $dateElement); + $type = $dateElement->getType(); + $this->assertEquals('DateType', $type->getName()); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type); + + $dateType = $schema->findType('DateType', 'spec:example:xsd:CommonBasicComponents-1.0'); + $this->assertNotNull($dateType); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $dateType); + + // defined in /schema/extend-components.xsd + $deliveryDateElement = $schema->findElement('DeliveryDate', 'spec:example:xsd:CommonBasicComponents-1.0'); + $this->assertNotNull($deliveryDateElement); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $deliveryDateElement); + $type = $deliveryDateElement->getType(); + $this->assertEquals('DateType', $type->getName()); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $type); + } + + /** + * Ensure Semantics of are the same as described in the XSD specification + * @link https://www.w3.org/TR/xmlschema11-1/#modify-schema + */ + public function testRedefineSemantics() + { + $schema = $this->reader->readFile(__DIR__.'/schema/extend-xsd-v2.xsd'); + $this->assertInstanceOf(Schema::class, $schema); + + // Definition from https://www.w3.org/TR/xmlschema11-1/#modify-schema + + // The schema corresponding to v2.xsd has everything specified by v1.xsd, with the personName type redefined, + // as well as everything it specifies itself. + // According to this schema, elements constrained by the personName type may end with a generation element. + // This includes not only the author element, but also the addressee element. + $author = $schema->findElement('author'); + $addressee = $schema->findElement('addressee'); + $this->assertNotNull($author); + $this->assertNotNull($addressee); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $author); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef::class, $addressee); + $authorType = $author->getType(); + $addresseeType = $addressee->getType(); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $authorType); + $this->assertInstanceOf(\GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType::class, $addresseeType); + + $this->assertEquals('personName', $authorType->getName()); + $this->assertEquals('personName', $addresseeType->getName()); + + // ensure both types contain the same elements + foreach([$authorType, $addresseeType] as $type) { + /** @var $type \GoetasWebservices\XML\XSDReader\Schema\Type\ComplexType */ + + $elements = $type->getElements(); + $elementNames = array_map(function(Element $element) { return $element->getName(); }, $elements); + sort($elementNames); + $this->assertEquals(['forename', 'generation', 'title'], $elementNames); + } + + + // + // For any document D2 pointed at by a element in D1, it must be the case either (a) that tns(D1) = tns(D2) or else (b) that tns(D2) is ·absent·, in which case schema(D1) includes not redefine(E,schema(D2)) itself but redefine(E,schema(chameleon(tns(D1),D2))). That is, the redefinition pre-processing is applied not to the schema corresponding to D2 but instead to the schema corresponding to the schema document chameleon(tns(D1),D2), which is the result of applying chameleon pre-processing to D2 to convert it to target namespace tns(D1). + + } +} diff --git a/tests/schema/base-components.xsd b/tests/schema/base-components.xsd new file mode 100644 index 00000000..50a9d536 --- /dev/null +++ b/tests/schema/base-components.xsd @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/tests/schema/extend-components.xsd b/tests/schema/extend-components.xsd new file mode 100644 index 00000000..290f3205 --- /dev/null +++ b/tests/schema/extend-components.xsd @@ -0,0 +1,19 @@ + + + + + + + + Lieferdatum + + + + Indikator + + + \ No newline at end of file diff --git a/tests/schema/redefine-xsd-v1.xsd b/tests/schema/redefine-xsd-v1.xsd new file mode 100644 index 00000000..76a8edb0 --- /dev/null +++ b/tests/schema/redefine-xsd-v1.xsd @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/schema/redefine-xsd-v2.xsd b/tests/schema/redefine-xsd-v2.xsd new file mode 100644 index 00000000..a0cb5144 --- /dev/null +++ b/tests/schema/redefine-xsd-v2.xsd @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +