Daniel Kinzler has uploaded a new change for review.
https://gerrit.wikimedia.org/r/199975
Change subject: RdfWriter: fix sub-writers
......................................................................
RdfWriter: fix sub-writers
This introduces logic for "posting" content to a future
location in the output buffer, after the current statement.
This logic is somewhat tricky, and for internal use only.
The intention is to allow sub-writers to be created in
situations when they would be out of sync syntactically
otherwise.
Change-Id: I6eab41bdc333f2b82653f73e159252e6f259128e
---
M repo/includes/rdf/N3RdfWriterBase.php
M repo/includes/rdf/NTriplesRdfWriter.php
M repo/includes/rdf/RdfWriter.php
M repo/includes/rdf/RdfWriterBase.php
M repo/includes/rdf/TurtleRdfWriter.php
M repo/includes/rdf/XmlRdfWriter.php
M repo/tests/phpunit/data/rdf/Predicates.nt
M repo/tests/phpunit/data/rdf/Predicates.rdf
M repo/tests/phpunit/data/rdf/Predicates.ttl
M repo/tests/phpunit/includes/rdf/RdfWriterTestBase.php
10 files changed, 218 insertions(+), 46 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikibase
refs/changes/75/199975/1
diff --git a/repo/includes/rdf/N3RdfWriterBase.php
b/repo/includes/rdf/N3RdfWriterBase.php
index a6fb496..331eb2c 100644
--- a/repo/includes/rdf/N3RdfWriterBase.php
+++ b/repo/includes/rdf/N3RdfWriterBase.php
@@ -43,22 +43,16 @@
protected function writeRef( $base, $local = null ) {
if ( $local === null ) {
- if ( $this->isShorthand( $base ) ) {
- $this->writeShorthand( $base );
+ if ( !is_string( $base ) ) {
+ $this->write( $base ); // callbacks,
sub-writers, etc
+ } elseif( $this->isShorthand( $base ) ) {
+ $this->write( $base );
} else {
$this->writeIRI( $base );
}
} else {
$this->writeQName( $base, $local );
}
- }
-
- protected function writeShorthand( $shorthand ) {
- if ( $shorthand === null || $shorthand === '' ) {
- throw new InvalidArgumentException( '$shorthand must
not be empty' );
- }
-
- $this->write( $shorthand );
}
protected function writeIRI( $iri ) {
diff --git a/repo/includes/rdf/NTriplesRdfWriter.php
b/repo/includes/rdf/NTriplesRdfWriter.php
index d0263a5..2ecedb0 100644
--- a/repo/includes/rdf/NTriplesRdfWriter.php
+++ b/repo/includes/rdf/NTriplesRdfWriter.php
@@ -89,6 +89,12 @@
protected function newSubWriter( $role, BNodeLabeler $labeler ) {
$writer = new self( $role, $labeler, $this->quoter );
+ if ( $role === self::STATEMENT_ROLE ) {
+ // Create a expanding writer with two writer streams,
one for direct statements
+ // and one for reified statements.
+ $writer = new RdrExpandingWriter( $writer,
$writer->sub() );
+ }
+
return $writer;
}
diff --git a/repo/includes/rdf/RdfWriter.php b/repo/includes/rdf/RdfWriter.php
index 3facc0a..cbae676 100644
--- a/repo/includes/rdf/RdfWriter.php
+++ b/repo/includes/rdf/RdfWriter.php
@@ -101,7 +101,7 @@
*
* @return RdfWriter $this
*/
- public function say( $base, $local );
+ public function say( $base, $local = null );
/**
* Produce a resource as the object of a statement.
diff --git a/repo/includes/rdf/RdfWriterBase.php
b/repo/includes/rdf/RdfWriterBase.php
index 50f63da..d8e66d2 100644
--- a/repo/includes/rdf/RdfWriterBase.php
+++ b/repo/includes/rdf/RdfWriterBase.php
@@ -19,7 +19,8 @@
abstract class RdfWriterBase implements RdfWriter {
/**
- * @var array An array of strings or RdfWriters.
+ * Output buffer of this writer. Call drain() to retrieve the value and
reset the buffer.
+ * @var array An array of strings, RdfWriters, or Closures.
*/
private $buffer = array();
@@ -29,6 +30,14 @@
const STATE_PREDICATE = 11;
const STATE_OBJECT = 12;
const STATE_DRAIN = 100;
+
+ /**
+ * Items to be added to $this->buffer next time a state() transition
allows
+ * output on the top level of the document, syntactically between
complete triples.
+ *
+ * @var array An array of strings, RdfWriters, or Closures.
+ */
+ private $backlog = array();
/**
* @var string the current state
@@ -79,6 +88,11 @@
* Role ID for writers that will generate a single inline RDR statement.
*/
const STATEMENT_ROLE = 'statement';
+
+ /**
+ * Role ID for writers that will generate a document snippet.
+ */
+ const SUBDOCUMENT_ROLE = 'sub';
/**
* @var string The writer's role, see the XXX_ROLE constants.
@@ -144,6 +158,10 @@
* @return bool
*/
protected function isShorthand( $shorthand ) {
+ if ( !is_string( $shorthand ) ) {
+ return false;
+ }
+
return isset( $this->shorthands[$shorthand] );
}
@@ -171,17 +189,19 @@
* @return RdfWriter
*/
final public function sub() {
- //FIXME: don't mess with the state, enqueue the writer to be
placed in the buffer
- // later, on the next transtion to subject|document|drain
- $this->state( self::STATE_DOCUMENT );
+ $writer = $this->newSubWriter( self::SUBDOCUMENT_ROLE,
$this->labeler );
- $writer = $this->newSubWriter( self::DOCUMENT_ROLE,
$this->labeler );
- $writer->state = self::STATE_DOCUMENT;
+ if ( $writer instanceof RdfWriterBase ) {
+ //FIXME: find a better way than instanceof!
+ $writer->state = self::STATE_DOCUMENT;
- // share registered prefixes
- $writer->prefixes =& $this->prefixes;
+ // share registered prefixes
+ $writer->prefixes =& $this->prefixes;
+ }
- $this->write( $writer );
+ // Insert the output of $writer at the next position that is
syntactically
+ // and structurally between subjects.
+ $this->post( $writer );
return $writer;
}
@@ -205,6 +225,35 @@
final protected function write() {
foreach ( func_get_args() as $arg ) {
$this->buffer[] = $arg;
+ }
+ }
+
+ /**
+ * Posts output to the backlog, to be committed to the buffer by a call
to commitBacklog().
+ * commitBacklog() is called when state allows, that is between
subjects, after finishSubject()
+ * and before finishDocument().
+ *
+ * @param string|RdfBuffer|Closure ... any number of output elements to
place in the backlog.
+ */
+ final protected function post() {
+ if ( $this->state === self::STATE_START || $this->state ===
self::STATE_DOCUMENT || $this->state === self::STATE_DRAIN ) {
+ // If the state allows, write directly to $this->buffer
+ $buffer = &$this->buffer;
+ } else {
+ $buffer = &$this->backlog;
+ }
+
+ $numArgs = func_num_args();
+
+ for ( $i = 0; $i < $numArgs; $i++ ) {
+ $s = func_get_arg( $i );
+ $buffer[] = $s;
+ }
+ }
+
+ private function commitBacklog() {
+ foreach ( $this->backlog as $item ) {
+ $this->buffer[] = $item;
}
}
@@ -509,13 +558,14 @@
break;
case self::STATE_START:
- $this->beginDocument();
+ $this->beginDocument( $this->role );
break;
case self::STATE_OBJECT: // when injecting a
sub-document
$this->finishObject( 'last' );
$this->finishPredicate( 'last' );
- $this->finishSubject();
+ $this->finishSubject( $this->role );
+ $this->commitBacklog();
break;
default:
@@ -526,18 +576,19 @@
private function transitionSubject() {
switch ( $this->state ) {
case self::STATE_DOCUMENT:
- $this->beginSubject();
+ $this->beginSubject( $this->role );
break;
case self::STATE_OBJECT:
- if ( $this->role !== self::DOCUMENT_ROLE ) {
+ if ( $this->role !== self::DOCUMENT_ROLE &&
$this->role !== self::SUBDOCUMENT_ROLE ) {
throw new LogicException( 'Bad
transition: ' . $this->state. ' -> ' . self::STATE_SUBJECT );
}
$this->finishObject( 'last' );
$this->finishPredicate( 'last' );
- $this->finishSubject();
- $this->beginSubject();
+ $this->finishSubject( $this->role );
+ $this->commitBacklog();
+ $this->beginSubject( $this->role );
break;
default:
@@ -586,19 +637,25 @@
private function transitionDrain() {
switch ( $this->state ) {
+ case self::STATE_DRAIN:
+ break;
+
case self::STATE_START:
+ $this->commitBacklog();
break;
case self::STATE_DOCUMENT:
- $this->finishDocument();
+ $this->commitBacklog();
+ $this->finishDocument( $this->role );
break;
case self::STATE_OBJECT:
$this->finishObject( 'last' );
$this->finishPredicate( 'last' );
- $this->finishSubject();
- $this->finishDocument();
+ $this->finishSubject( $this->role );
+ $this->commitBacklog();
+ $this->finishDocument( $this->role );
break;
default:
@@ -675,31 +732,33 @@
/**
* May be implemented to generate any output that may be needed at the
beginning of a
* document.
+ *
+ * @param string $role the writer's role, see the XXX_ROLE constants.
*/
- protected function beginDocument() {
+ protected function beginDocument( $role ) {
}
/**
* May be implemented to generate any output that may be needed at the
end of a
* document (e.g. this would generate "</rdf:RDF>" for RDF/XML).
*/
- protected function finishDocument() {
+ protected function finishDocument( $role ) {
}
/**
* May be implemented to generate any output that may be needed at the
beginning of a
* sequence of statements about a subject.
*
- * @param bool $first Whether this is the first statement in the
document.
+ * @param string $role the writer's role, see the XXX_ROLE constants.
*/
- protected function beginSubject( $first = false ) {
+ protected function beginSubject( $role ) {
}
/**
* May be implemented to generate any output that may be needed at the
end of a
* sequence of statements about a single subject (e.g. this would
generate "." for Turtle).
*/
- protected function finishSubject() {
+ protected function finishSubject( $role ) {
}
/**
diff --git a/repo/includes/rdf/TurtleRdfWriter.php
b/repo/includes/rdf/TurtleRdfWriter.php
index a2a87a9..ae2c62a 100644
--- a/repo/includes/rdf/TurtleRdfWriter.php
+++ b/repo/includes/rdf/TurtleRdfWriter.php
@@ -36,12 +36,28 @@
parent::writeValue( $value, $typeBase, $typeLocal );
}
- protected function beginSubject( $first = false ) {
- $this->write( "\n" );
+ protected function beginDocument( $role ) {
+ if ( $role === self::STATEMENT_ROLE ) {
+ $this->write( '<< ' );
+ }
}
- protected function finishSubject() {
- $this->write( " .\n" );
+ protected function finishDocument( $role ) {
+ if ( $role === self::STATEMENT_ROLE ) {
+ $this->write( ' >>' );
+ }
+ }
+
+ protected function beginSubject( $role ) {
+ if ( $role !== self::STATEMENT_ROLE ) {
+ $this->write( "\n" ); //TODO: configure wrap & indent
by role
+ }
+ }
+
+ protected function finishSubject( $role ) {
+ if ( $role !== self::STATEMENT_ROLE ) {
+ $this->write( " .\n" );
+ }
}
protected function beginPredicate( $first = false ) {
@@ -51,7 +67,8 @@
}
protected function finishPredicate( $last = false ) {
- if ( !$last ) {
+ //FIXME: pass $role as an argument, avoid function call!
+ if ( !$last && $this->getRole() !== self::STATEMENT_ROLE ) {
$this->write( " ;\n\t" );
}
}
diff --git a/repo/includes/rdf/XmlRdfWriter.php
b/repo/includes/rdf/XmlRdfWriter.php
index 2017252..144b2c4 100644
--- a/repo/includes/rdf/XmlRdfWriter.php
+++ b/repo/includes/rdf/XmlRdfWriter.php
@@ -17,7 +17,11 @@
}
private function escape( $text ) {
- return htmlspecialchars( $text, ENT_QUOTES );
+ if ( is_object( $text ) ) {
+ return $text;
+ } else {
+ return htmlspecialchars( $text, ENT_QUOTES );
+ }
}
protected function expandSubject( &$base, &$local ) {
@@ -102,7 +106,11 @@
/**
* Emit a document header.
*/
- protected function beginDocument() {
+ protected function beginDocument( $role ) {
+ if ( $role !== self::DOCUMENT_ROLE ) {
+ return;
+ }
+
$this->write( '<?xml version="1.0"?>', "\n" );
// define a callback for generating namespace attributes
@@ -139,16 +147,18 @@
/**
* Emit the root element
*/
- protected function finishSubject() {
+ protected function finishSubject( $role ) {
$this->write( "\t" );
$this->close( 'rdf', 'Description' );
$this->write( "\n" );
}
- protected function finishDocument() {
- // close document element
- $this->close( 'rdf', 'RDF' );
- $this->write( "\n" );
+ protected function finishDocument( $role ) {
+ if ( $role === self::DOCUMENT_ROLE ) {
+ // close document element
+ $this->close( 'rdf', 'RDF' );
+ $this->write( "\n" );
+ }
}
protected function writePredicate( $base, $local = null ) {
diff --git a/repo/tests/phpunit/data/rdf/Predicates.nt
b/repo/tests/phpunit/data/rdf/Predicates.nt
index 40d76b1..ccc49c0 100644
--- a/repo/tests/phpunit/data/rdf/Predicates.nt
+++ b/repo/tests/phpunit/data/rdf/Predicates.nt
@@ -2,3 +2,4 @@
<http://foobar.test/Bananas> <http://acme.test/name> "Banana" .
<http://foobar.test/Bananas> <http://acme.test/name> "Banane"@de .
<http://foobar.test/Apples> <http://acme.test/name> "Apple" .
+<http://foobar.test/Peaches> <http://acme.test/looks> "pink" .
diff --git a/repo/tests/phpunit/data/rdf/Predicates.rdf
b/repo/tests/phpunit/data/rdf/Predicates.rdf
index b8da7f9..ac6f108 100644
--- a/repo/tests/phpunit/data/rdf/Predicates.rdf
+++ b/repo/tests/phpunit/data/rdf/Predicates.rdf
@@ -8,4 +8,7 @@
<rdf:Description rdf:about="http://foobar.test/Apples">
<name>Apple</name>
</rdf:Description>
+ <rdf:Description rdf:about="http://foobar.test/Peaches">
+ <looks>pink</looks>
+ </rdf:Description>
</rdf:RDF>
\ No newline at end of file
diff --git a/repo/tests/phpunit/data/rdf/Predicates.ttl
b/repo/tests/phpunit/data/rdf/Predicates.ttl
index ac0f16a..3e46959 100644
--- a/repo/tests/phpunit/data/rdf/Predicates.ttl
+++ b/repo/tests/phpunit/data/rdf/Predicates.ttl
@@ -6,3 +6,5 @@
"Banane"@de .
<http://foobar.test/Apples> :name "Apple" .
+
+<http://foobar.test/Peaches> :looks "pink" .
diff --git a/repo/tests/phpunit/includes/rdf/RdfWriterTestBase.php
b/repo/tests/phpunit/includes/rdf/RdfWriterTestBase.php
index 8d8f212..7f04645 100644
--- a/repo/tests/phpunit/includes/rdf/RdfWriterTestBase.php
+++ b/repo/tests/phpunit/includes/rdf/RdfWriterTestBase.php
@@ -80,6 +80,82 @@
$this->assertOutputLines( 'Triples', $rdf );
}
+ public function testSub_first() {
+ $writer = $this->newWriter();
+
+ $writer->start();
+ $writer->prefix( 'acme', 'http://acme.test/' );
+
+ // the sub-writer is committed directly
+ $fruit = $writer->sub();
+
+ // interspersed prefix definition
+ $writer->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' );
+
+ $writer->about( 'acme', 'Nuts' )
+ ->say( 'acme', 'weight' )->value( '5.5', 'xsd',
'decimal' )
+ ->say( 'acme', 'color' )->value( 'brown' );
+
+ // generate the output for $fruit out of sequence
+ $fruit->about( 'http://foobar.test/Bananas' )
+ ->say( 'a' )->is( 'http://foobar.test/Fruit' );
+
+ $rdf = $writer->drain();
+ $this->assertOutputLines( 'Triples', $rdf );
+ }
+
+ public function testSub_after_subject() {
+ $writer = $this->newWriter();
+
+ $writer->start();
+ $writer->prefix( 'acme', 'http://acme.test/' );
+
+ $writer->about( 'http://foobar.test/Bananas' );
+
+ // Create sub-writer between subject and predicate, output
should go after.
+ $nuts = $writer->sub();
+ $nuts->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' );
+ $nuts->about( 'acme', 'Nuts' )
+ ->say( 'acme', 'weight' )->value( '5.5', 'xsd',
'decimal' )
+ ->say( 'acme', 'color' )->value( 'brown' );
+
+ // continue to talk about bananas using the top level writer
+ $writer->say( 'a' )->is( 'http://foobar.test/Fruit' ); //
shorthand name "a"
+
+ // Commit of backlog is triggered here
+ $rdf = $writer->drain();
+ $this->assertOutputLines( 'Triples', $rdf );
+ }
+
+ public function testSub_inline() {
+ $writer = $this->newWriter();
+
+ $writer->start();
+ $writer->prefix( '', 'http://acme.test/' ); // empty prefix
+ $writer->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' );
+
+ $writer->about( 'http://foobar.test/Bananas' )
+ ->a( 'http://foobar.test/Fruit' ) // shorthand function
a()
+ ->say( '', 'name' ) // empty prefix
+ ->text( 'Banana' );
+
+ // Sub-writer generated between objects.
+ $writer->sub()->about( 'http://foobar.test/Apples' )
+ ->say( '', 'name' ) // subsequent call to say( '',
'name' ) for a different subject
+ ->text( 'Apple' );
+
+ $writer->text( 'Banane', 'de' ); // another object for Bananas
+
+ // Output about apples should show up here
+
+ $writer->about( 'http://foobar.test/Peaches' )
+ ->say( '', 'looks' )
+ ->text( 'pink' );
+
+ $rdf = $writer->drain();
+ $this->assertOutputLines( 'Predicates', $rdf );
+ }
+
public function testPredicates() {
$writer = $this->newWriter();
@@ -98,6 +174,10 @@
->say( '', 'name' ) // subsequent call to say( '',
'name' ) for a different subject
->text( 'Apple' );
+ $writer->about( 'http://foobar.test/Peaches' )
+ ->say( '', 'looks' )
+ ->text( 'pink' );
+
$rdf = $writer->drain();
$this->assertOutputLines( 'Predicates', $rdf );
}
--
To view, visit https://gerrit.wikimedia.org/r/199975
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I6eab41bdc333f2b82653f73e159252e6f259128e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikibase
Gerrit-Branch: master
Gerrit-Owner: Daniel Kinzler <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits