]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - vendor/wikimedia/purtle/src/RdfWriterBase.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / wikimedia / purtle / src / RdfWriterBase.php
1 <?php
2
3 namespace Wikimedia\Purtle;
4
5 use Closure;
6 use InvalidArgumentException;
7 use LogicException;
8
9 /**
10  * Base class for RdfWriter implementations.
11  *
12  * Subclasses have to implement at least the writeXXX() methods to generate the desired output
13  * for the respective RDF constructs. Subclasses may override the startXXX() and finishXXX()
14  * methods to generate structural output, and override expandXXX() to transform identifiers.
15  *
16  * @license GPL-2.0+
17  * @author Daniel Kinzler
18  */
19 abstract class RdfWriterBase implements RdfWriter {
20
21         /**
22          * @var array An array of strings, RdfWriters, or closures.
23          */
24         private $buffer = [];
25
26         /**
27          * @var RdfWriter[] sub-writers.
28          */
29         private $subs = [];
30
31         const STATE_START = 0;
32         const STATE_DOCUMENT = 5;
33         const STATE_SUBJECT = 10;
34         const STATE_PREDICATE = 11;
35         const STATE_OBJECT = 12;
36         const STATE_FINISH = 666;
37
38         /**
39          * @var string the current state
40          */
41         private $state = self::STATE_START;
42
43         /**
44          * Shorthands that can be used in place of IRIs, e.g. ("a" to mean rdf:type).
45          *
46          * @var string[] a map of shorthand names to [ $base, $local ] pairs.
47          * @todo Handle "a" as a special case directly. Use for custom "variables" like %currentValue
48          *  instead.
49          */
50         private $shorthands = [];
51
52         /**
53          * @var string[] a map of prefixes to base IRIs
54          */
55         private $prefixes = [];
56
57         /**
58          * @var array pair to store the current subject.
59          * Holds the $base and $local parameters passed to about().
60          */
61         protected $currentSubject = [ null, null ];
62
63         /**
64          * @var array pair to store the current predicate.
65          * Holds the $base and $local parameters passed to say().
66          */
67         protected $currentPredicate = [ null, null ];
68
69         /**
70          * @var BNodeLabeler
71          */
72         private $labeler;
73
74         /**
75          * Role ID for writers that will generate a full RDF document.
76          */
77         const DOCUMENT_ROLE = 'document';
78         const SUBDOCUMENT_ROLE = 'sub';
79
80         /**
81          * Role ID for writers that will generate a single inline blank node.
82          */
83         const BNODE_ROLE = 'bnode';
84
85         /**
86          * Role ID for writers that will generate a single inline RDR statement.
87          */
88         const STATEMENT_ROLE = 'statement';
89
90         /**
91          * @var string The writer's role, see the XXX_ROLE constants.
92          */
93         protected $role;
94
95         /**
96          * Are prefixed locked against modification?
97          * @var bool
98          */
99         private $prefixesLocked = false;
100
101         /**
102          * @param string $role The writer's role, use the XXX_ROLE constants.
103          * @param BNodeLabeler|null $labeler
104          *
105          * @throws InvalidArgumentException
106          */
107         public function __construct( $role, BNodeLabeler $labeler = null ) {
108                 if ( !is_string( $role ) ) {
109                         throw new InvalidArgumentException( '$role must be a string' );
110                 }
111
112                 $this->role = $role;
113                 $this->labeler = $labeler ?: new BNodeLabeler();
114
115                 $this->registerShorthand( 'a', 'rdf', 'type' );
116
117                 $this->prefix( 'rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' );
118                 $this->prefix( 'xsd', 'http://www.w3.org/2001/XMLSchema#' );
119         }
120
121         /**
122          * @param string $role
123          * @param BNodeLabeler $labeler
124          *
125          * @return RdfWriterBase
126          */
127         abstract protected function newSubWriter( $role, BNodeLabeler $labeler );
128
129         /**
130          * Registers a shorthand that can be used instead of a qname,
131          * like 'a' can be used instead of 'rdf:type'.
132          *
133          * @param string $shorthand
134          * @param string $prefix
135          * @param string $local
136          */
137         protected function registerShorthand( $shorthand, $prefix, $local ) {
138                 $this->shorthands[$shorthand] = [ $prefix, $local ];
139         }
140
141         /**
142          * Registers a prefix
143          *
144          * @param string $prefix
145          * @param string $iri The base IRI
146          *
147          * @throws LogicException
148          */
149         public function prefix( $prefix, $iri ) {
150                 if ( $this->prefixesLocked ) {
151                         throw new LogicException( 'Prefixes can not be added after start()' );
152                 }
153
154                 $this->prefixes[$prefix] = $iri;
155         }
156
157         /**
158          * Determines whether $shorthand can be used as a shorthand.
159          *
160          * @param string $shorthand
161          *
162          * @return bool
163          */
164         protected function isShorthand( $shorthand ) {
165                 return isset( $this->shorthands[$shorthand] );
166         }
167
168         /**
169          * Determines whether $shorthand can legally be used as a prefix.
170          *
171          * @param string $prefix
172          *
173          * @return bool
174          */
175         protected function isPrefix( $prefix ) {
176                 return isset( $this->prefixes[$prefix] );
177         }
178
179         /**
180          * Returns the prefix map.
181          *
182          * @return string[] An associative array mapping prefixes to base IRIs.
183          */
184         public function getPrefixes() {
185                 return $this->prefixes;
186         }
187
188         /**
189          * @param string|null $languageCode
190          *
191          * @return bool
192          */
193         protected function isValidLanguageCode( $languageCode ) {
194                 // preg_match is somewhat (12%) slower than strspn but more readable
195                 return $languageCode !== null && preg_match( '/^[\da-z-]{2,}$/i', $languageCode );
196         }
197
198         /**
199          * @return RdfWriter
200          */
201         final public function sub() {
202                 $writer = $this->newSubWriter( self::SUBDOCUMENT_ROLE, $this->labeler );
203                 $writer->state = self::STATE_DOCUMENT;
204
205                 // share registered prefixes
206                 $writer->prefixes =& $this->prefixes;
207
208                 $this->subs[] = $writer;
209                 return $writer;
210         }
211
212         /**
213          * Returns the writers role. The role determines the behavior of the writer with respect
214          * to which states and transitions are possible: a BNODE_ROLE writer would for instance
215          * not accept a call to about(), since it can only process triples about a single subject
216          * (the blank node it represents).
217          *
218          * @return string A string corresponding to one of the the XXX_ROLE constants.
219          */
220         final public function getRole() {
221                 return $this->role;
222         }
223
224         /**
225          * Appends string to the output buffer.
226          * @param string $w
227          */
228         final protected function write( $w ) {
229                 $this->buffer[] = $w;
230         }
231
232         /**
233          * If $base is a shorthand, $base and $local are updated to hold whatever qname
234          * the shorthand was associated with.
235          *
236          * Otherwise, $base and $local remain unchanged.
237          *
238          * @param string &$base
239          * @param string|null &$local
240          */
241         protected function expandShorthand( &$base, &$local ) {
242                 if ( $local === null && isset( $this->shorthands[$base] ) ) {
243                         list( $base, $local ) = $this->shorthands[$base];
244                 }
245         }
246
247         /**
248          * If $base is a registered prefix, $base will be replaced by the base IRI associated with
249          * that prefix, with $local appended. $local will be set to null.
250          *
251          * Otherwise, $base and $local remain unchanged.
252          *
253          * @param string &$base
254          * @param string|null &$local
255          *
256          * @throws LogicException
257          */
258         protected function expandQName( &$base, &$local ) {
259                 if ( $local !== null && $base !== '_' ) {
260                         if ( isset( $this->prefixes[$base] ) ) {
261                                 $base = $this->prefixes[$base] . $local; //XXX: can we avoid this concat?
262                                 $local = null;
263                         } else {
264                                 throw new LogicException( 'Unknown prefix: ' . $base );
265                         }
266                 }
267         }
268
269         /**
270          * @see RdfWriter::blank()
271          *
272          * @param string|null $label node label, will be generated if not given.
273          *
274          * @return string
275          */
276         final public function blank( $label = null ) {
277                 return $this->labeler->getLabel( $label );
278         }
279
280         /**
281          * @see RdfWriter::start()
282          */
283         final public function start() {
284                 $this->state( self::STATE_DOCUMENT );
285                 $this->prefixesLocked = true;
286         }
287
288         /**
289          * @see RdfWriter::finish()
290          */
291         final public function finish() {
292                 // close all unclosed states
293                 $this->state( self::STATE_DOCUMENT );
294
295                 // ...then insert output of sub-writers into the buffer,
296                 // so it gets placed before the footer...
297                 $this->drainSubs();
298
299                 // and then finalize
300                 $this->state( self::STATE_FINISH );
301
302                 // Detaches all subs.
303                 $this->subs = [];
304         }
305
306         /**
307          * @see RdfWriter::drain()
308          *
309          * @return string RDF
310          */
311         final public function drain() {
312                 // we can drain after finish, but finish state is sticky
313                 if ( $this->state !== self::STATE_FINISH ) {
314                         $this->state( self::STATE_DOCUMENT );
315                 }
316
317                 $this->drainSubs();
318                 $this->flattenBuffer();
319
320                 $rdf = join( '', $this->buffer );
321                 $this->buffer = [];
322
323                 return $rdf;
324         }
325
326         /**
327          * Calls drain() an any RdfWriter instances in $this->buffer, and replaces them
328          * in $this->buffer with the string returned by the drain() call. Any closures
329          * present in the $this->buffer will be called, and replaced by their return value.
330          */
331         private function flattenBuffer() {
332                 foreach ( $this->buffer as &$b ) {
333                         if ( $b instanceof Closure ) {
334                                 $b = $b();
335                         }
336                         if ( $b instanceof RdfWriter ) {
337                                 $b = $b->drain();
338                         }
339                 }
340         }
341
342         /**
343          * Drains all subwriters, and appends their output to this writer's buffer.
344          * Subwriters remain usable.
345          */
346         private function drainSubs() {
347                 foreach ( $this->subs as $sub ) {
348                         $rdf = $sub->drain();
349                         $this->write( $rdf );
350                 }
351         }
352
353         /**
354          * @see RdfWriter::about()
355          *
356          * @param string $base A QName prefix if $local is given, or an IRI if $local is null.
357          * @param string|null $local A QName suffix, or null if $base is an IRI.
358          *
359          * @return RdfWriter $this
360          */
361         final public function about( $base, $local = null ) {
362                 $this->expandSubject( $base, $local );
363
364                 if ( $this->state === self::STATE_OBJECT
365                         && $base === $this->currentSubject[0]
366                         && $local === $this->currentSubject[1]
367                 ) {
368                         return $this; // redundant about() call
369                 }
370
371                 $this->state( self::STATE_SUBJECT );
372
373                 $this->currentSubject[0] = $base;
374                 $this->currentSubject[1] = $local;
375                 $this->currentPredicate[0] = null;
376                 $this->currentPredicate[1] = null;
377
378                 $this->writeSubject( $base, $local );
379                 return $this;
380         }
381
382         /**
383          * @see RdfWriter::a()
384          * Shorthand for say( 'a' )->is( $type ).
385          *
386          * @param string $typeBase The data type's QName prefix if $typeLocal is given,
387          *        or an IRI or shorthand if $typeLocal is null.
388          * @param string|null $typeLocal The data type's  QName suffix,
389          *        or null if $typeBase is an IRI or shorthand.
390          *
391          * @return RdfWriter $this
392          */
393         final public function a( $typeBase, $typeLocal = null ) {
394                 return $this->say( 'a' )->is( $typeBase, $typeLocal );
395         }
396
397         /**
398          * @see RdfWriter::say()
399          *
400          * @param string $base A QName prefix.
401          * @param string|null $local A QName suffix.
402          *
403          * @return RdfWriter $this
404          */
405         final public function say( $base, $local = null ) {
406                 $this->expandPredicate( $base, $local );
407
408                 if ( $this->state === self::STATE_OBJECT
409                         && $base === $this->currentPredicate[0]
410                         && $local === $this->currentPredicate[1]
411                 ) {
412                         return $this; // redundant about() call
413                 }
414
415                 $this->state( self::STATE_PREDICATE );
416
417                 $this->currentPredicate[0] = $base;
418                 $this->currentPredicate[1] = $local;
419
420                 $this->writePredicate( $base, $local );
421                 return $this;
422         }
423
424         /**
425          * @see RdfWriter::is()
426          *
427          * @param string $base A QName prefix if $local is given, or an IRI if $local is null.
428          * @param string|null $local A QName suffix, or null if $base is an IRI.
429          *
430          * @return RdfWriter $this
431          */
432         final public function is( $base, $local = null ) {
433                 $this->state( self::STATE_OBJECT );
434
435                 $this->expandResource( $base, $local );
436                 $this->writeResource( $base, $local );
437                 return $this;
438         }
439
440         /**
441          * @see RdfWriter::text()
442          *
443          * @param string $text the text to be placed in the output
444          * @param string|null $language the language the text is in
445          *
446          * @return $this
447          */
448         final public function text( $text, $language = null ) {
449                 $this->state( self::STATE_OBJECT );
450
451                 $this->writeText( $text, $language );
452                 return $this;
453         }
454
455         /**
456          * @see RdfWriter::value()
457          *
458          * @param string $value the value encoded as a string
459          * @param string|null $typeBase The data type's QName prefix if $typeLocal is given,
460          *        or an IRI or shorthand if $typeLocal is null.
461          * @param string|null $typeLocal The data type's  QName suffix,
462          *        or null if $typeBase is an IRI or shorthand.
463          *
464          * @return $this
465          */
466         final public function value( $value, $typeBase = null, $typeLocal = null ) {
467                 $this->state( self::STATE_OBJECT );
468
469                 if ( $typeBase === null && !is_string( $value ) ) {
470                         $vtype = gettype( $value );
471                         switch ( $vtype ) {
472                                 case 'integer':
473                                         $typeBase = 'xsd';
474                                         $typeLocal = 'integer';
475                                         $value = "$value";
476                                         break;
477
478                                 case 'double':
479                                         $typeBase = 'xsd';
480                                         $typeLocal = 'double';
481                                         $value = "$value";
482                                         break;
483
484                                 case 'boolean':
485                                         $typeBase = 'xsd';
486                                         $typeLocal = 'boolean';
487                                         $value = $value ? 'true' : 'false';
488                                         break;
489                         }
490                 }
491
492                 $this->expandType( $typeBase, $typeLocal );
493
494                 $this->writeValue( $value, $typeBase, $typeLocal );
495                 return $this;
496         }
497
498         /**
499          * State transition table
500          * First state is "from", second is "to"
501          * @var array
502          */
503         protected $transitionTable = [
504                         self::STATE_START => [
505                                         self::STATE_DOCUMENT => true,
506                         ],
507                         self::STATE_DOCUMENT => [
508                                         self::STATE_DOCUMENT => true,
509                                         self::STATE_SUBJECT => true,
510                                         self::STATE_FINISH => true,
511                         ],
512                         self::STATE_SUBJECT => [
513                                         self::STATE_PREDICATE => true,
514                         ],
515                         self::STATE_PREDICATE => [
516                                         self::STATE_OBJECT => true,
517                         ],
518                         self::STATE_OBJECT => [
519                                         self::STATE_DOCUMENT => true,
520                                         self::STATE_SUBJECT => true,
521                                         self::STATE_PREDICATE => true,
522                                         self::STATE_OBJECT => true,
523                         ],
524         ];
525
526         /**
527          * Perform a state transition. Writer states roughly correspond to states in a naive
528          * regular parser for the respective syntax. State transitions may generate output,
529          * particularly of structural elements which correspond to terminals in a respective
530          * parser.
531          *
532          * @param int $newState one of the self::STATE_... constants
533          *
534          * @throws LogicException
535          */
536         final protected function state( $newState ) {
537                 if ( !isset( $this->transitionTable[$this->state][$newState] ) ) {
538                         throw new LogicException( 'Bad transition: ' . $this->state . ' -> ' . $newState );
539                 }
540
541                 $action = $this->transitionTable[$this->state][$newState];
542                 if ( $action !== true ) {
543                         if ( is_string( $action ) ) {
544                                 $this->write( $action );
545                         } else {
546                                 $action();
547                         }
548                 }
549
550                 $this->state = $newState;
551         }
552
553         /**
554          * Must be implemented to generate output that starts a statement (or set of statements)
555          * about a subject. Depending on the requirements of the output format, the implementation
556          * may be empty.
557          *
558          * @note: $base and $local are given as passed to about() and processed by expandSubject().
559          *
560          * @param string $base
561          * @param string|null $local
562          */
563         abstract protected function writeSubject( $base, $local = null );
564
565         /**
566          * Must be implemented to generate output that represents the association of a predicate
567          * with a subject that was previously defined by a call to writeSubject().
568          *
569          * @note: $base and $local are given as passed to say() and processed by expandPredicate().
570          *
571          * @param string $base
572          * @param string|null $local
573          */
574         abstract protected function writePredicate( $base, $local = null );
575
576         /**
577          * Must be implemented to generate output that represents a resource used as the object
578          * of a statement.
579          *
580          * @note: $base and $local are given as passed to is() and processed by expandObject().
581          *
582          * @param string $base
583          * @param string|null $local
584          */
585         abstract protected function writeResource( $base, $local = null );
586
587         /**
588          * Must be implemented to generate output that represents a text used as the object
589          * of a statement.
590          *
591          * @param string $text the text to be placed in the output
592          * @param string|null $language the language the text is in
593          */
594         abstract protected function writeText( $text, $language );
595
596         /**
597          * Must be implemented to generate output that represents a (typed) literal used as the object
598          * of a statement.
599          *
600          * @note: $typeBase and $typeLocal are given as passed to value() and processed by expandType().
601          *
602          * @param string $value the value encoded as a string
603          * @param string $typeBase
604          * @param string|null $typeLocal
605          */
606         abstract protected function writeValue( $value, $typeBase, $typeLocal = null );
607
608         /**
609          * Perform any expansion (shorthand to qname, qname to IRI) desired
610          * for subject identifiers.
611          *
612          * @param string &$base
613          * @param string|null &$local
614          */
615         protected function expandSubject( &$base, &$local ) {
616         }
617
618         /**
619          * Perform any expansion (shorthand to qname, qname to IRI) desired
620          * for predicate identifiers.
621          *
622          * @param string &$base
623          * @param string|null &$local
624          */
625         protected function expandPredicate( &$base, &$local ) {
626         }
627
628         /**
629          * Perform any expansion (shorthand to qname, qname to IRI) desired
630          * for resource identifiers.
631          *
632          * @param string &$base
633          * @param string|null &$local
634          */
635         protected function expandResource( &$base, &$local ) {
636         }
637
638         /**
639          * Perform any expansion (shorthand to qname, qname to IRI) desired
640          * for type identifiers.
641          *
642          * @param string &$base
643          * @param string|null &$local
644          */
645         protected function expandType( &$base, &$local ) {
646         }
647
648 }