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

Reply via email to