diff --git a/Graph.php b/Graph.php index cfd67a7..4593781 100644 --- a/Graph.php +++ b/Graph.php @@ -169,6 +169,56 @@ public function removeFromDocument() $doc->removeGraph($this); } + /** + * {@inheritdoc} + */ + public function merge(GraphInterface $graph) + { + $nodes = $graph->getNodes(); + $bnodeMap = array(); + + foreach ($nodes as $node) { + if ($node->isBlankNode()) { + if (false === isset($bnodeMap[$node->getId()])) { + $bnodeMap[$node->getId()] = $this->createNode(); + } + $n = $bnodeMap[$node->getId()]; + } else { + $n = $this->createNode($node->getId()); + } + + foreach ($node->getProperties() as $property => $values) { + if (false === is_array($values)) { + $values = array($values); + } + + foreach ($values as $val) { + if ($val instanceof NodeInterface) { + // If the value is another node, we just need to + // create a reference to the corresponding node + // in this graph. The properties will be merged + // in the outer loop + if ($val->isBlankNode()) { + if (false === isset($bnodeMap[$val->getId()])) { + $bnodeMap[$val->getId()] = $this->createNode(); + } + $val = $bnodeMap[$val->getId()]; + } else { + $val = $this->createNode($val->getId()); + } + } elseif (is_object($val)) { + // Clone typed values and language-tagged strings + $val = clone $val; + } + + $n->addPropertyValue($property, $val); + } + } + } + + return $this; + } + /** * Create a new blank node identifier unique to the document. * diff --git a/GraphInterface.php b/GraphInterface.php index df5c9d8..f7414d9 100644 --- a/GraphInterface.php +++ b/GraphInterface.php @@ -96,4 +96,14 @@ public function getDocument(); * Removes the graph from the document */ public function removeFromDocument(); + + /** + * Merges the specified graph into the current graph + * + * @param GraphInterface $graph The graph that should be merged into the + * current graph. + * + * @return self + */ + public function merge(GraphInterface $graph); } diff --git a/Test/GraphTest.php b/Test/GraphTest.php index cf4ab2c..78a141b 100644 --- a/Test/GraphTest.php +++ b/Test/GraphTest.php @@ -18,11 +18,11 @@ use ML\JsonLD\TypedValue; /** - * Test the parsing of a JSON-LD document into a Document. + * Test the parsing of a JSON-LD document into a Graph. * * @author Markus Lanthaler */ -class DocumentTest extends \PHPUnit_Framework_TestCase +class GraphTest extends \PHPUnit_Framework_TestCase { /** * The graph instance being used throughout the tests. @@ -42,7 +42,7 @@ protected function setUp() "ex": "http://vocab.com/", "ex:lang": { "@language": "en" }, "ex:typed": { "@type": "ex:type/datatype" }, - "node": "ex:type/node" + "Node": "ex:type/node" }, "@graph": [ { @@ -68,7 +68,7 @@ protected function setUp() { "@id": "http://example.com/node/3", "ex:name": "3", - "@type": "node", + "@type": "Node", "ex:link": { "@id": "http://example.com/node/1" }, "ex:contains": { "@id": "_:someBlankNode", @@ -122,11 +122,11 @@ public function testGetNodes() // Is the node of the right type? $this->assertInstanceOf('ML\JsonLD\Node', $node); - // Does the document return the same instance? + // Does the graph return the same instance? $n = $this->graph->getNode($node->getId()); $this->assertSame($node, $n, 'same instance'); $this->assertTrue($node->equals($n), 'equals'); - $this->assertSame($this->graph, $n->getGraph(), 'linked to document'); + $this->assertSame($this->graph, $n->getGraph(), 'linked to graph'); } } @@ -267,7 +267,7 @@ public function testNodeReverseRelationshipsUpdated() } /** - * Tests the removal of nodes from the document. + * Tests the removal of nodes from the graph. */ public function testNodeRemoval() { @@ -276,7 +276,7 @@ public function testNodeRemoval() $node1_1 = $this->graph->getNode('_:b0'); $node2 = $this->graph->getNode('http://example.com/node/2'); - $this->assertTrue($this->graph->containsNode('http://example.com/node/1'), 'node 1 in document?'); + $this->assertTrue($this->graph->containsNode('http://example.com/node/1'), 'node 1 in graph?'); $this->assertSame( array('http://vocab.com/link' => array($node1)), @@ -298,8 +298,8 @@ public function testNodeRemoval() $this->assertSame(array(), $node1_1->getReverseProperties(), 'n1.1 reverse properties'); $this->assertNull($node1_1->getReverseProperty('http://vocab.com/contains'), 'n1.1 <-contains- n1 removed'); - $this->assertFalse($this->graph->containsNode('/node/1'), 'node 1 still in document?'); - $this->assertNull($node1->getGraph(), 'node 1\'s document reset?'); + $this->assertFalse($this->graph->containsNode('/node/1'), 'node 1 still in graph?'); + $this->assertNull($node1->getGraph(), 'node 1\'s graph reset?'); // Remove node 2 $node2 = $this->graph->getNode('http://example.com/node/2'); @@ -307,7 +307,7 @@ public function testNodeRemoval() $node2_2 = $this->graph->getNode('_:b2'); $node3 = $this->graph->getNode('/node/3'); - $this->assertTrue($this->graph->containsNode('/node/2'), 'node 2 in document?'); + $this->assertTrue($this->graph->containsNode('/node/2'), 'node 2 in graph?'); $this->assertSame( array('http://vocab.com/link' => array($node2)), @@ -338,11 +338,11 @@ public function testNodeRemoval() $this->assertSame(array(), $node2_2->getReverseProperties(), 'n2.2 reverse properties'); $this->assertNull($node2_2->getReverseProperty('http://vocab.com/contains'), 'n2.2 <-contains- n2 removed'); - $this->assertFalse($this->graph->containsNode('./2'), 'node 2 still in document?'); + $this->assertFalse($this->graph->containsNode('./2'), 'node 2 still in graph?'); } /** - * Tests the removal of node types from the document. + * Tests the removal of node types from the graph. */ public function testNodeTypeRemoval() { @@ -351,7 +351,7 @@ public function testNodeTypeRemoval() $node3 = $this->graph->getNode('/node/3'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); - $this->assertTrue($this->graph->containsNode('http://vocab.com/type/node'), 'node type in document?'); + $this->assertTrue($this->graph->containsNode('http://vocab.com/type/node'), 'node type in graph?'); $this->assertSame($nodeType, $node1->getType(), 'n1 type'); $this->assertSame($nodeType, $node3->getType(), 'n3 type'); @@ -370,7 +370,7 @@ public function testNodeTypeRemoval() $this->assertNull($node1->getType(), 'n1 type removed'); $this->assertNull($node3->getType(), 'n3 type removed'); - $this->assertFalse($this->graph->containsNode('http://vocab.com/type/node'), 'node type still in document?'); + $this->assertFalse($this->graph->containsNode('http://vocab.com/type/node'), 'node type still in graph?'); } /** @@ -488,10 +488,10 @@ public function testNodePropertyUniqueness() $this->assertSame($node1, $value, 'node: initial node'); $newNode1 = $this->graph->createNode(); - $this->assertTrue($this->graph->containsNode($newNode1), 'node: new1 in document'); + $this->assertTrue($this->graph->containsNode($newNode1), 'node: new1 in graph'); $newNode2 = $this->graph->createNode('http://example.com/node/new/2'); - $this->assertTrue($this->graph->containsNode($newNode2), 'node: new2 in document'); + $this->assertTrue($this->graph->containsNode($newNode2), 'node: new2 in graph'); $node->addPropertyValue('http://vocab.com/link', $node1); $this->assertSame($node1, $node->getProperty('http://vocab.com/link'), 'node: still same'); @@ -521,7 +521,7 @@ public function testNodePropertyUniqueness() $this->assertSame($nodeType, $node1->getType(), 'type: n1 initial type'); $newType1 = $this->graph->createNode(); - $this->assertTrue($this->graph->containsNode($newNode1), 'type: new1 in document'); + $this->assertTrue($this->graph->containsNode($newNode1), 'type: new1 in graph'); $node1->addType($nodeType); $this->assertSame($nodeType, $node1->getType(), 'type: n1 type still same'); @@ -591,11 +591,11 @@ public function testSetInvalidTypeArray() /** * Tests whether it is possible to add an type which is not part of the - * document + * graph * * @expectedException InvalidArgumentException */ - public function testAddTypeNotInDocument() + public function testAddTypeNotInGraph() { $graph = new Graph(); $newType = $graph->createNode(); @@ -605,7 +605,7 @@ public function testAddTypeNotInDocument() } /** - * Tests whether nodes are contained in the document + * Tests whether nodes are contained in the graph */ public function testContains() { @@ -631,4 +631,129 @@ public function testCreateExistingNode() $this->assertSame($node1, $this->graph->createNode('http://example.com/node/1')); $this->assertSame($nodeType, $this->graph->createNode('http://vocab.com/type/node')); } + + /** + * Tests the merging of two graphs + */ + public function testMerge() + { + + $json = << 'http://example.com/node/index.jsonld'))->getGraph(); + + // Merge graph2 into graph + $this->graph->merge($graph2); + + $nodeIds = array( + 'http://example.com/node/1', + 'http://example.com/node/2', + 'http://example.com/node/3', + 'http://example.com/node/4', + '_:b0', + '_:b1', + '_:b2', + '_:b3', + '_:b4', + 'http://vocab.com/type/datatype', + 'http://vocab.com/type/node', + 'http://vocab.com/type/nodeWithAliases' + ); + + $nodes = $this->graph->getNodes(); + $this->assertCount(count($nodeIds), $nodes); + + foreach ($nodes as $node) { + // Is the node's ID valid? + $this->assertContains($node->getId(), $nodeIds, 'Found unexpected node ID: ' . $node->getId()); + + // Is the node of the right type? + $this->assertInstanceOf('ML\JsonLD\Node', $node); + + // Does the graph return the same instance? + $n = $this->graph->getNode($node->getId()); + $this->assertSame($node, $n, 'same instance'); + $this->assertTrue($node->equals($n), 'equals'); + $this->assertSame($this->graph, $n->getGraph(), 'linked to graph'); + + // It must not share node objects with graph 2 + $this->assertNotSame($node, $graph2->getNode($node->getId()), 'shared instance between graph and graph 2'); + } + + // Check that the properties have been updated as well + $node1 = $this->graph->getNode('http://example.com/node/1'); + $node2 = $this->graph->getNode('http://example.com/node/2'); + $node3 = $this->graph->getNode('http://example.com/node/3'); + $node4 = $this->graph->getNode('http://example.com/node/4'); + + $this->assertEquals('1', $node1->getProperty('http://vocab.com/name'), 'n1->name'); + $this->assertSame($node2, $node1->getProperty('http://vocab.com/link'), 'n1 -link-> n2'); + $this->assertCount(2, $node1->getProperty('http://vocab.com/contains'), 'n1 -contains-> 2 blank nodes'); + + $this->assertEquals( + array('2', 'and a different name in graph 2'), + $node2->getProperty('http://vocab.com/name'), 'n2->name' + ); + + $this->assertSame(array($node3, $node4), $node2->getProperty('http://vocab.com/link'), 'n2 -link-> n3 & n4'); + $this->assertEquals( + 'this was added in graph 2', + $node2->getProperty('http://vocab.com/newFromGraph2'), + 'n2->newFromGraph2' + ); + + $this->assertEquals('node 4 from graph 2', $node4->getProperty('http://vocab.com/name'), 'n4->name'); + + // Verify that graph 2 wasn't changed + $nodeIds = array( + 'http://example.com/node/1', + 'http://example.com/node/2', + '_:b0', // ex:contains: { ex:nested } + 'http://example.com/node/4', + 'http://vocab.com/type/node' + ); + + $nodes = $graph2->getNodes(); + $this->assertCount(count($nodeIds), $nodes); + + foreach ($nodes as $node) { + // Is the node's ID valid? + $this->assertContains($node->getId(), $nodeIds, 'Found unexpected node ID in graph 2: ' . $node->getId()); + + // Is the node of the right type? + $this->assertInstanceOf('ML\JsonLD\Node', $node); + + // Does the graph return the same instance? + $n = $graph2->getNode($node->getId()); + $this->assertSame($node, $n, 'same instance (graph 2)'); + $this->assertTrue($node->equals($n), 'equals (graph 2)'); + $this->assertSame($graph2, $n->getGraph(), 'linked to graph (graph 2)'); + } + } }