Minor dependency updates
[yaffs-website] / vendor / easyrdf / easyrdf / lib / EasyRdf / Parser / Turtle.php
1 <?php
2
3 /**
4  * EasyRdf
5  *
6  * LICENSE
7  *
8  * Copyright (c) 2009-2013 Nicholas J Humfrey.
9  * Copyright (c) 1997-2013 Aduna (http://www.aduna-software.com/)
10  * All rights reserved.
11  *
12  * Redistribution and use in source and binary forms, with or without
13  * modification, are permitted provided that the following conditions are met:
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright notice,
17  *    this list of conditions and the following disclaimer in the documentation
18  *    and/or other materials provided with the distribution.
19  * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
20  *    promote products derived from this software without specific prior
21  *    written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33  * POSSIBILITY OF SUCH DAMAGE.
34  *
35  * @package    EasyRdf
36  * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
37  *             Copyright (c) 1997-2006 Aduna (http://www.aduna-software.com/)
38  * @license    http://www.opensource.org/licenses/bsd-license.php
39  */
40
41 /**
42  * Class to parse Turtle with no external dependancies.
43  *
44  * It is a translation from Java to PHP of the Sesame Turtle Parser:
45  * http://bit.ly/TurtleParser
46  * 
47  * Lasted updated against version: 
48  * ecda6a15a200a2fc6a062e2e43081257c3ccd4e6   (Mon Jul 29 12:05:58 2013)
49  * 
50  * @package    EasyRdf
51  * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
52  *             Copyright (c) 1997-2013 Aduna (http://www.aduna-software.com/)
53  * @license    http://www.opensource.org/licenses/bsd-license.php
54  */
55 class EasyRdf_Parser_Turtle extends EasyRdf_Parser_Ntriples
56 {
57     protected $data;
58     protected $namespaces;
59     protected $subject;
60     protected $predicate;
61     protected $object;
62     
63     protected $line;
64     protected $column;
65
66     /**
67      * Constructor
68      *
69      * @return object EasyRdf_Parser_Turtle
70      */
71     public function __construct()
72     {
73     }
74
75     /**
76      * Parse Turtle into an EasyRdf_Graph
77      *
78      * @param object EasyRdf_Graph $graph   the graph to load the data into
79      * @param string               $data    the RDF document data
80      * @param string               $format  the format of the input data
81      * @param string               $baseUri the base URI of the data being parsed
82      * @return integer             The number of triples added to the graph
83      */
84     public function parse($graph, $data, $format, $baseUri)
85     {
86         parent::checkParseParams($graph, $data, $format, $baseUri);
87
88         if ($format != 'turtle') {
89             throw new EasyRdf_Exception(
90                 "EasyRdf_Parser_Turtle does not support: $format"
91             );
92         }
93
94         $this->data = $data;
95         $this->namespaces = array();
96         $this->subject = null;
97         $this->predicate = null;
98         $this->object = null;
99         
100         $this->line = 1;
101         $this->column = 1;
102
103         $this->resetBnodeMap();
104
105         $c = $this->skipWSC();
106         while ($c != -1) {
107             $this->parseStatement();
108             $c = $this->skipWSC();
109         }
110
111         return $this->tripleCount;
112     }
113
114
115     /**
116      * Parse a statement [2]
117      * @ignore
118      */
119     protected function parseStatement()
120     {
121         $directive = '';
122         while (true) {
123             $c = $this->read();
124             if ($c == -1 || self::isWhitespace($c)) {
125                 $this->unread($c);
126                 break;
127             } else {
128                 $directive .= $c;
129             }
130         }
131
132         if (preg_match('/^(@|prefix$|base$)/i', $directive)) {
133             $this->parseDirective($directive);
134             $this->skipWSC();
135             // SPARQL BASE and PREFIX lines do not end in .
136             if ($directive[0] == "@") {
137                 $this->verifyCharacterOrFail($this->read(), ".");
138             }
139         } else {
140             $this->unread($directive);
141             $this->parseTriples();
142             $this->skipWSC();
143             $this->verifyCharacterOrFail($this->read(), ".");
144         }
145     }
146
147     /**
148      * Parse a directive [3]
149      * @ignore
150      */
151     protected function parseDirective($directive)
152     {
153         $directive = strtolower($directive);
154         if ($directive == "prefix" || $directive == '@prefix') {
155             $this->parsePrefixID();
156         } elseif ($directive == "base" || $directive == '@base') {
157             $this->parseBase();
158         } elseif (mb_strlen($directive, "UTF-8") == 0) {
159             throw new EasyRdf_Parser_Exception(
160                 "Turtle Parse Error: directive name is missing, expected @prefix or @base",
161                 $this->line,
162                 $this->column
163             );
164         } else {
165             throw new EasyRdf_Parser_Exception(
166                 "Turtle Parse Error: unknown directive \"$directive\"",
167                 $this->line,
168                 $this->column
169             );
170         }
171     }
172
173     /**
174      * Parse a prefixID [4]
175      * @ignore
176      */
177     protected function parsePrefixID()
178     {
179         $this->skipWSC();
180
181         // Read prefix ID (e.g. "rdf:" or ":")
182         $prefixID = '';
183
184         while (true) {
185             $c = $this->read();
186             if ($c == ':') {
187                 $this->unread($c);
188                 break;
189             } elseif (self::isWhitespace($c)) {
190                 break;
191             } elseif ($c == -1) {
192                 throw new EasyRdf_Parser_Exception(
193                     "Turtle Parse Error: unexpected end of file while reading prefix id",
194                     $this->line,
195                     $this->column
196                 );
197             }
198
199             $prefixID .= $c;
200         }
201
202         $this->skipWSC();
203         $this->verifyCharacterOrFail($this->read(), ":");
204         $this->skipWSC();
205
206         // Read the namespace URI
207         $namespace = $this->parseURI();
208
209         // Store local namespace mapping
210         $this->namespaces[$prefixID] = $namespace['value'];
211     }
212
213     /**
214      * Parse base [5]
215      * @ignore
216      */
217     protected function parseBase()
218     {
219         $this->skipWSC();
220
221         $baseUri = $this->parseURI();
222         $this->baseUri = new EasyRdf_ParsedUri($baseUri['value']);
223     }
224
225     /**
226      * Parse triples [6]
227      * @ignore
228      */
229     protected function parseTriples()
230     {
231         $c = $this->peek();
232
233         // If the first character is an open bracket we need to decide which of
234         // the two parsing methods for blank nodes to use
235         if ($c == '[') {
236             $c = $this->read();
237             $this->skipWSC();
238             $c = $this->peek();
239             if ($c == ']') {
240                 $c = $this->read();
241                 $this->subject = $this->createBNode();
242                 $this->skipWSC();
243                 $this->parsePredicateObjectList();
244             } else {
245                 $this->unread('[');
246                 $this->subject = $this->parseImplicitBlank();
247             }
248             $this->skipWSC();
249             $c = $this->peek();
250
251             // if this is not the end of the statement, recurse into the list of
252             // predicate and objects, using the subject parsed above as the subject
253             // of the statement.
254             if ($c != '.') {
255                 $this->parsePredicateObjectList();
256             }
257         } else {
258             $this->parseSubject();
259             $this->skipWSC();
260             $this->parsePredicateObjectList();
261         }
262
263         $this->subject = null;
264         $this->predicate = null;
265         $this->object = null;
266     }
267
268     /**
269      * Parse a predicateObjectList [7]
270      * @ignore
271      */
272     protected function parsePredicateObjectList()
273     {
274         $this->predicate = $this->parsePredicate();
275
276         $this->skipWSC();
277         $this->parseObjectList();
278
279         while ($this->skipWSC() == ';') {
280             $this->read();
281
282             $c = $this->skipWSC();
283
284             if ($c == '.' || $c == ']') {
285                 break;
286             } elseif ($c == ';') {
287                 // empty predicateObjectList, skip to next
288                 continue;
289             }
290
291             $this->predicate = $this->parsePredicate();
292
293             $this->skipWSC();
294
295             $this->parseObjectList();
296         }
297     }
298
299     /**
300      * Parse a objectList [8]
301      * @ignore
302      */
303     protected function parseObjectList()
304     {
305         $this->parseObject();
306
307         while ($this->skipWSC() == ',') {
308             $this->read();
309             $this->skipWSC();
310             $this->parseObject();
311         }
312     }
313
314     /**
315      * Parse a subject [10]
316      * @ignore
317      */
318     protected function parseSubject()
319     {
320         $c = $this->peek();
321         if ($c == '(') {
322             $this->subject = $this->parseCollection();
323         } elseif ($c == '[') {
324             $this->subject = $this->parseImplicitBlank();
325         } else {
326             $value = $this->parseValue();
327
328             if ($value['type'] == 'uri' or $value['type'] == 'bnode') {
329                 $this->subject = $value;
330             } else {
331                 throw new EasyRdf_Parser_Exception(
332                     "Turtle Parse Error: illegal subject type: ".$value['type'],
333                     $this->line,
334                     $this->column
335                 );
336             }
337         }
338     }
339
340     /**
341      * Parse a predicate [11]
342      * @ignore
343      */
344     protected function parsePredicate()
345     {
346         // Check if the short-cut 'a' is used
347         $c1 = $this->read();
348
349         if ($c1 == 'a') {
350             $c2 = $this->read();
351
352             if (self::isWhitespace($c2)) {
353                 // Short-cut is used, return the rdf:type URI
354                 return array(
355                     'type' => 'uri',
356                     'value' => EasyRdf_Namespace::get('rdf') . 'type'
357                 );
358             }
359
360             // Short-cut is not used, unread all characters
361             $this->unread($c2);
362         }
363         $this->unread($c1);
364
365         // Predicate is a normal resource
366         $predicate = $this->parseValue();
367         if ($predicate['type'] == 'uri') {
368             return $predicate;
369         } else {
370             throw new EasyRdf_Parser_Exception(
371                 "Turtle Parse Error: Illegal predicate type: " . $predicate['type'],
372                 $this->line,
373                 $this->column
374             );
375         }
376     }
377
378     /**
379      * Parse a object [12]
380      * @ignore
381      */
382     protected function parseObject()
383     {
384         $c = $this->peek();
385
386         if ($c == '(') {
387             $this->object = $this->parseCollection();
388         } elseif ($c == '[') {
389             $this->object = $this->parseImplicitBlank();
390         } else {
391             $this->object = $this->parseValue();
392         }
393
394         $this->addTriple(
395             $this->subject['value'],
396             $this->predicate['value'],
397             $this->object
398         );
399     }
400
401     /**
402      * Parses a blankNodePropertyList [15]
403      *
404      * This method parses the token []
405      * and predicateObjectLists that are surrounded by square brackets.
406      *
407      * @ignore
408      */
409     protected function parseImplicitBlank()
410     {
411         $this->verifyCharacterOrFail($this->read(), "[");
412
413         $bnode = $this->createBNode();
414
415         $c = $this->read();
416         if ($c != ']') {
417             $this->unread($c);
418
419             // Remember current subject and predicate
420             $oldSubject = $this->subject;
421             $oldPredicate = $this->predicate;
422
423             // generated bNode becomes subject
424             $this->subject = $bnode;
425
426             // Enter recursion with nested predicate-object list
427             $this->skipWSC();
428
429             $this->parsePredicateObjectList();
430
431             $this->skipWSC();
432
433             // Read closing bracket
434             $this->verifyCharacterOrFail($this->read(), "]");
435
436             // Restore previous subject and predicate
437             $this->subject = $oldSubject;
438             $this->predicate = $oldPredicate;
439         }
440
441         return $bnode;
442     }
443
444     /**
445      * Parses a collection [16], e.g: ( item1 item2 item3 )
446      * @ignore
447      */
448     protected function parseCollection()
449     {
450         $this->verifyCharacterOrFail($this->read(), "(");
451
452         $c = $this->skipWSC();
453         if ($c == ')') {
454             // Empty list
455             $this->read();
456             return array(
457                 'type' => 'uri',
458                 'value' => EasyRdf_Namespace::get('rdf') . 'nil'
459             );
460         } else {
461             $listRoot = $this->createBNode();
462
463             // Remember current subject and predicate
464             $oldSubject = $this->subject;
465             $oldPredicate = $this->predicate;
466
467             // generated bNode becomes subject, predicate becomes rdf:first
468             $this->subject = $listRoot;
469             $this->predicate = array(
470                 'type' => 'uri',
471                 'value' => EasyRdf_Namespace::get('rdf') . 'first'
472             );
473
474             $this->parseObject();
475             $bNode = $listRoot;
476
477             while ($this->skipWSC() != ')') {
478                 // Create another list node and link it to the previous
479                 $newNode = $this->createBNode();
480
481                 $this->addTriple(
482                     $bNode['value'],
483                     EasyRdf_Namespace::get('rdf') . 'rest',
484                     $newNode
485                 );
486
487                 // New node becomes the current
488                 $this->subject = $bNode = $newNode;
489
490                 $this->parseObject();
491             }
492
493             // Skip ')'
494             $this->read();
495
496             // Close the list
497             $this->addTriple(
498                 $bNode['value'],
499                 EasyRdf_Namespace::get('rdf') . 'rest',
500                 array(
501                     'type' => 'uri',
502                     'value' => EasyRdf_Namespace::get('rdf') . 'nil'
503                 )
504             );
505
506             // Restore previous subject and predicate
507             $this->subject = $oldSubject;
508             $this->predicate = $oldPredicate;
509
510             return $listRoot;
511         }
512     }
513
514     /**
515      * Parses an RDF value. This method parses uriref, qname, node ID, quoted
516      * literal, integer, double and boolean.
517      * @ignore
518      */
519     protected function parseValue()
520     {
521         $c = $this->peek();
522
523         if ($c == '<') {
524             // uriref, e.g. <foo://bar>
525             return $this->parseURI();
526         } elseif ($c == ':' || self::isPrefixStartChar($c)) {
527             // qname or boolean
528             return $this->parseQNameOrBoolean();
529         } elseif ($c == '_') {
530             // node ID, e.g. _:n1
531             return $this->parseNodeID();
532         } elseif ($c == '"' || $c == "'") {
533             // quoted literal, e.g. "foo" or """foo""" or 'foo' or '''foo'''
534             return $this->parseQuotedLiteral();
535         } elseif (ctype_digit($c) || $c == '.' || $c == '+' || $c == '-') {
536             // integer or double, e.g. 123 or 1.2e3
537             return $this->parseNumber();
538         } elseif ($c == -1) {
539             throw new EasyRdf_Parser_Exception(
540                 "Turtle Parse Error: unexpected end of file while reading value",
541                 $this->line,
542                 $this->column
543             );
544         } else {
545             throw new EasyRdf_Parser_Exception(
546                 "Turtle Parse Error: expected an RDF value here, found '$c'",
547                 $this->line,
548                 $this->column
549             );
550         }
551     }
552
553     /**
554      * Parses a quoted string, optionally followed by a language tag or datatype.
555      * @ignore
556      */
557     protected function parseQuotedLiteral()
558     {
559         $label = $this->parseQuotedString();
560
561         // Check for presence of a language tag or datatype
562         $c = $this->peek();
563
564         if ($c == '@') {
565             $this->read();
566
567             // Read language
568             $lang = '';
569             $c = $this->read();
570             if ($c == -1) {
571                 throw new EasyRdf_Parser_Exception(
572                     "Turtle Parse Error: unexpected end of file while reading language",
573                     $this->line,
574                     $this->column
575                 );
576             } elseif (!self::isLanguageStartChar($c)) {
577                 throw new EasyRdf_Parser_Exception(
578                     "Turtle Parse Error: expected a letter, found '$c'",
579                     $this->line,
580                     $this->column
581                 );
582             }
583
584             $lang .= $c;
585
586             $c = $this->read();
587             while (!self::isWhitespace($c)) {
588                 if ($c == '.' || $c == ';' || $c == ',' || $c == ')' || $c == ']' || $c == -1) {
589                     break;
590                 }
591                 if (self::isLanguageChar($c)) {
592                     $lang .= $c;
593                 } else {
594                     throw new EasyRdf_Parser_Exception(
595                         "Turtle Parse Error: illegal language tag char: '$c'",
596                         $this->line,
597                         $this->column
598                     );
599                 }
600                 $c = $this->read();
601             }
602
603             $this->unread($c);
604
605             return array(
606                 'type' => 'literal',
607                 'value' => $label,
608                 'lang' => $lang
609             );
610         } elseif ($c == '^') {
611             $this->read();
612
613             // next character should be another '^'
614             $this->verifyCharacterOrFail($this->read(), "^");
615
616             // Read datatype
617             $datatype = $this->parseValue();
618             if ($datatype['type'] == 'uri') {
619                 return array(
620                     'type' => 'literal',
621                     'value' => $label,
622                     'datatype' => $datatype['value']
623                 );
624             } else {
625                 throw new EasyRdf_Parser_Exception(
626                     "Turtle Parse Error: illegal datatype type: " . $datatype['type'],
627                     $this->line,
628                     $this->column
629                 );
630             }
631         } else {
632             return array(
633                 'type' => 'literal',
634                 'value' => $label
635             );
636         }
637     }
638
639     /**
640      * Parses a quoted string, which is either a "normal string" or a """long string""".
641      * @ignore
642      */
643     protected function parseQuotedString()
644     {
645         $result = null;
646
647         $c1 = $this->read();
648
649         // First character should be ' or "
650         $this->verifyCharacterOrFail($c1, "\"\'");
651
652         // Check for long-string, which starts and ends with three double quotes
653         $c2 = $this->read();
654         $c3 = $this->read();
655
656         if ($c2 == $c1 && $c3 == $c1) {
657             // Long string
658             $result = $this->parseLongString($c2);
659         } else {
660             // Normal string
661             $this->unread($c3);
662             $this->unread($c2);
663
664             $result = $this->parseString($c1);
665         }
666
667         // Unescape any escape sequences
668         return $this->unescapeString($result);
669     }
670
671     /**
672      * Parses a "normal string". This method requires that the opening character
673      * has already been parsed.
674      * @param string  $closingCharacter  The type of quote to use (either ' or ")
675      * @ignore
676      */
677     protected function parseString($closingCharacter)
678     {
679         $str = '';
680
681         while (true) {
682             $c = $this->read();
683
684             if ($c == $closingCharacter) {
685                 break;
686             } elseif ($c == -1) {
687                 throw new EasyRdf_Parser_Exception(
688                     "Turtle Parse Error: unexpected end of file while reading string",
689                     $this->line,
690                     $this->column
691                 );
692             }
693
694             $str .= $c;
695
696             if ($c == '\\') {
697                 // This escapes the next character, which might be a ' or a "
698                 $c = $this->read();
699                 if ($c == -1) {
700                     throw new EasyRdf_Parser_Exception(
701                         "Turtle Parse Error: unexpected end of file while reading string",
702                         $this->line,
703                         $this->column
704                     );
705                 }
706                 $str .= $c;
707             }
708         }
709
710         return $str;
711     }
712
713     /**
714      * Parses a """long string""". This method requires that the first three
715      * characters have already been parsed.
716      * @param string  $closingCharacter  The type of quote to use (either ' or ")
717      * @ignore
718      */
719     protected function parseLongString($closingCharacter)
720     {
721         $str = '';
722         $doubleQuoteCount = 0;
723
724         while ($doubleQuoteCount < 3) {
725             $c = $this->read();
726
727             if ($c == -1) {
728                 throw new EasyRdf_Parser_Exception(
729                     "Turtle Parse Error: unexpected end of file while reading long string",
730                     $this->line,
731                     $this->column
732                 );
733             } elseif ($c == $closingCharacter) {
734                 $doubleQuoteCount++;
735             } else {
736                 $doubleQuoteCount = 0;
737             }
738
739             $str .= $c;
740
741             if ($c == '\\') {
742                 // This escapes the next character, which might be a ' or "
743                 $c = $this->read();
744                 if ($c == -1) {
745                     throw new EasyRdf_Parser_Exception(
746                         "Turtle Parse Error: unexpected end of file while reading long string",
747                         $this->line,
748                         $this->column
749                     );
750                 }
751                 $str .= $c;
752             }
753         }
754
755         return mb_substr($str, 0, -3, "UTF-8");
756     }
757
758     /**
759      * Parses a numeric value, either of type integer, decimal or double
760      * @ignore
761      */
762     protected function parseNumber()
763     {
764         $value = '';
765         $datatype = EasyRdf_Namespace::get('xsd').'integer';
766
767         $c = $this->read();
768
769         // read optional sign character
770         if ($c == '+' || $c == '-') {
771             $value .= $c;
772             $c = $this->read();
773         }
774
775         while (ctype_digit($c)) {
776             $value .= $c;
777             $c = $this->read();
778         }
779
780         if ($c == '.' || $c == 'e' || $c == 'E') {
781             // read optional fractional digits
782             if ($c == '.') {
783
784                 if (self::isWhitespace($this->peek())) {
785                     // We're parsing an integer that did not have a space before the
786                     // period to end the statement
787                 } else {
788                     $value .= $c;
789                     $c = $this->read();
790                     while (ctype_digit($c)) {
791                         $value .= $c;
792                         $c = $this->read();
793                     }
794
795                     if (mb_strlen($value, "UTF-8") == 1) {
796                         // We've only parsed a '.'
797                         throw new EasyRdf_Parser_Exception(
798                             "Turtle Parse Error: object for statement missing",
799                             $this->line,
800                             $this->column
801                         );
802                     }
803
804                     // We're parsing a decimal or a double
805                     $datatype = EasyRdf_Namespace::get('xsd').'decimal';
806                 }
807             } else {
808                 if (mb_strlen($value, "UTF-8") == 0) {
809                     // We've only parsed an 'e' or 'E'
810                     throw new EasyRdf_Parser_Exception(
811                         "Turtle Parse Error: object for statement missing",
812                         $this->line,
813                         $this->column
814                     );
815                 }
816             }
817
818             // read optional exponent
819             if ($c == 'e' || $c == 'E') {
820                 $datatype = EasyRdf_Namespace::get('xsd').'double';
821                 $value .= $c;
822
823                 $c = $this->read();
824                 if ($c == '+' || $c == '-') {
825                     $value .= $c;
826                     $c = $this->read();
827                 }
828
829                 if (!ctype_digit($c)) {
830                     throw new EasyRdf_Parser_Exception(
831                         "Turtle Parse Error: exponent value missing",
832                         $this->line,
833                         $this->column
834                     );
835                 }
836
837                 $value .= $c;
838
839                 $c = $this->read();
840                 while (ctype_digit($c)) {
841                     $value .= $c;
842                     $c = $this->read();
843                 }
844             }
845         }
846
847         // Unread last character, it isn't part of the number
848         $this->unread($c);
849
850         // Return result as a typed literal
851         return array(
852             'type' => 'literal',
853             'value' => $value,
854             'datatype' => $datatype
855         );
856     }
857
858     /**
859      * Parses a URI / IRI
860      * @ignore
861      */
862     protected function parseURI()
863     {
864         $uri = '';
865
866         // First character should be '<'
867         $this->verifyCharacterOrFail($this->read(), "<");
868
869         // Read up to the next '>' character
870         while (true) {
871             $c = $this->read();
872
873             if ($c == '>') {
874                 break;
875             } elseif ($c == -1) {
876                 throw new EasyRdf_Parser_Exception(
877                     "Turtle Parse Error: unexpected end of file while reading URI",
878                     $this->line,
879                     $this->column
880                 );
881             }
882
883             $uri .= $c;
884
885             if ($c == '\\') {
886                 // This escapes the next character, which might be a '>'
887                 $c = $this->read();
888                 if ($c == -1) {
889                     throw new EasyRdf_Parser_Exception(
890                         "Turtle Parse Error: unexpected end of file while reading URI",
891                         $this->line,
892                         $this->column
893                     );
894                 }
895                 $uri .= $c;
896             }
897         }
898
899         // Unescape any escape sequences
900         $uri = $this->unescapeString($uri);
901
902         return array(
903             'type' => 'uri',
904             'value' => $this->resolve($uri)
905         );
906     }
907
908     /**
909      * Parses qnames and boolean values, which have equivalent starting
910      * characters.
911      * @ignore
912      */
913     protected function parseQNameOrBoolean()
914     {
915         // First character should be a ':' or a letter
916         $c = $this->read();
917         if ($c == -1) {
918             throw new EasyRdf_Parser_Exception(
919                 "Turtle Parse Error: unexpected end of file while readying value",
920                 $this->line,
921                 $this->column
922             );
923         }
924         if ($c != ':' && !self::isPrefixStartChar($c)) {
925             throw new EasyRdf_Parser_Exception(
926                 "Turtle Parse Error: expected a ':' or a letter, found '$c'",
927                 $this->line,
928                 $this->column
929             );
930         }
931
932         $namespace = null;
933
934         if ($c == ':') {
935             // qname using default namespace
936             if (isset($this->namespaces[''])) {
937                 $namespace = $this->namespaces[''];
938             } else {
939                 throw new EasyRdf_Parser_Exception(
940                     "Turtle Parse Error: default namespace used but not defined",
941                     $this->line,
942                     $this->column
943                 );
944             }
945         } else {
946             // $c is the first letter of the prefix
947             $prefix = $c;
948
949             $c = $this->read();
950             while (self::isPrefixChar($c)) {
951                 $prefix .= $c;
952                 $c = $this->read();
953             }
954
955             if ($c != ':') {
956                 // prefix may actually be a boolean value
957                 $value = $prefix;
958
959                 if ($value == "true" || $value == "false") {
960                     return array(
961                         'type' => 'literal',
962                         'value' => $value,
963                         'datatype' => EasyRdf_Namespace::get('xsd') . 'boolean'
964                     );
965                 }
966             }
967
968             $this->verifyCharacterOrFail($c, ":");
969
970             if (isset($this->namespaces[$prefix])) {
971                 $namespace = $this->namespaces[$prefix];
972             } else {
973                 throw new EasyRdf_Parser_Exception(
974                     "Turtle Parse Error: namespace prefix '$prefix' used but not defined",
975                     $this->line,
976                     $this->column
977                 );
978             }
979         }
980
981         // $c == ':', read optional local name
982         $localName = '';
983         $c = $this->read();
984         if (self::isNameStartChar($c)) {
985             if ($c == '\\') {
986                 $localName .= $this->readLocalEscapedChar();
987             } else {
988                 $localName .= $c;
989             }
990
991             $c = $this->read();
992             while (self::isNameChar($c)) {
993                 if ($c == '\\') {
994                     $localName .= $this->readLocalEscapedChar();
995                 } else {
996                     $localName .= $c;
997                 }
998                 $c = $this->read();
999             }
1000         }
1001
1002         // Unread last character
1003         $this->unread($c);
1004
1005         // Note: namespace has already been resolved
1006         return array(
1007             'type' => 'uri',
1008             'value' => $namespace . $localName
1009         );
1010     }
1011
1012     protected function readLocalEscapedChar()
1013     {
1014         $c = $this->read();
1015
1016         if (self::isLocalEscapedChar($c)) {
1017             return $c;
1018         } else {
1019             throw new EasyRdf_Parser_Exception(
1020                 "found '" . $c . "', expected one of: " . implode(', ', self::$localEscapedChars),
1021                 $this->line,
1022                 $this->column
1023             );
1024         }
1025     }
1026
1027     /**
1028      * Parses a blank node ID, e.g: _:node1
1029      * @ignore
1030      */
1031     protected function parseNodeID()
1032     {
1033         // Node ID should start with "_:"
1034         $this->verifyCharacterOrFail($this->read(), "_");
1035         $this->verifyCharacterOrFail($this->read(), ":");
1036
1037         // Read the node ID
1038         $c = $this->read();
1039         if ($c == -1) {
1040             throw new EasyRdf_Parser_Exception(
1041                 "Turtle Parse Error: unexpected end of file while reading node id",
1042                 $this->line,
1043                 $this->column
1044             );
1045         } elseif (!self::isNameStartChar($c)) {
1046             throw new EasyRdf_Parser_Exception(
1047                 "Turtle Parse Error: expected a letter, found '$c'",
1048                 $this->line,
1049                 $this->column
1050             );
1051         }
1052
1053         // Read all following letter and numbers, they are part of the name
1054         $name = $c;
1055         $c = $this->read();
1056         while (self::isNameChar($c)) {
1057             $name .= $c;
1058             $c = $this->read();
1059         }
1060
1061         $this->unread($c);
1062
1063         return array(
1064             'type' => 'bnode',
1065             'value' => $this->remapBnode($name)
1066         );
1067     }
1068
1069     protected function resolve($uri)
1070     {
1071         if ($this->baseUri) {
1072             return $this->baseUri->resolve($uri)->toString();
1073         } else {
1074             return $uri;
1075         }
1076     }
1077
1078     /**
1079      * Verifies that the supplied character $c is one of the expected
1080      * characters specified in $expected. This method will throw a
1081      * exception if this is not the case.
1082      * @ignore
1083      */
1084     protected function verifyCharacterOrFail($c, $expected)
1085     {
1086         if ($c == -1) {
1087             throw new EasyRdf_Parser_Exception(
1088                 "Turtle Parse Error: unexpected end of file",
1089                 $this->line,
1090                 $this->column
1091             );
1092         } elseif (strpbrk($c, $expected) === false) {
1093             $msg = 'expected ';
1094             for ($i = 0; $i < strlen($expected); $i++) {
1095                 if ($i > 0) {
1096                     $msg .= " or ";
1097                 }
1098                 $msg .= '\''.$expected[$i].'\'';
1099             }
1100             $msg .= ", found '$c'";
1101
1102             throw new EasyRdf_Parser_Exception(
1103                 "Turtle Parse Error: $msg",
1104                 $this->line,
1105                 $this->column
1106             );
1107         }
1108     }
1109
1110     /**
1111      * Skip through whitespace and comments
1112      * @ignore
1113      */
1114     protected function skipWSC()
1115     {
1116         $c = $this->read();
1117         while (self::isWhitespace($c) || $c == '#') {
1118             if ($c == '#') {
1119                 $this->processComment();
1120             }
1121
1122             $c = $this->read();
1123         }
1124
1125         $this->unread($c);
1126         return $c;
1127     }
1128
1129     /**
1130      * Consumes characters from reader until the first EOL has been read.
1131      * @ignore
1132      */
1133     protected function processComment()
1134     {
1135         $comment = '';
1136         $c = $this->read();
1137         while ($c != -1 && $c != "\r" && $c != "\n") {
1138             $comment .= $c;
1139             $c = $this->read();
1140         }
1141
1142         // c is equal to -1, \r or \n.
1143         // In case c is equal to \r, we should also read a following \n.
1144         if ($c == "\r") {
1145             $c = $this->read();
1146             if ($c != "\n") {
1147                 $this->unread($c);
1148             }
1149         }
1150     }
1151
1152     /**
1153      * Read a single character from the input buffer.
1154      * Returns -1 when the end of the file is reached.
1155      * @ignore
1156      */
1157     protected function read()
1158     {
1159         if (!empty($this->data)) {
1160             $c = mb_substr($this->data, 0, 1, "UTF-8");
1161             // Keep tracks of which line we are on (0A = Line Feed)
1162             if ($c == "\x0A") {
1163                 $this->line += 1;
1164                 $this->column = 1;
1165             } else {
1166                 $this->column += 1;
1167             }
1168
1169             if (version_compare(PHP_VERSION, '5.4.8', '<')) {
1170                 // versions of PHP prior to 5.4.8 treat "NULL" length parameter as 0
1171                 $this->data = mb_substr($this->data, 1, mb_strlen($this->data), "UTF-8");
1172             } else {
1173                 $this->data = mb_substr($this->data, 1, null, "UTF-8");
1174             }
1175             return $c;
1176         } else {
1177             return -1;
1178         }
1179     }
1180
1181     /**
1182      * Gets the next character to be returned by read()
1183      * without removing it from the input buffer.
1184      * @ignore
1185      */
1186     protected function peek()
1187     {
1188         if (!empty($this->data)) {
1189             return mb_substr($this->data, 0, 1, "UTF-8");
1190         } else {
1191             return -1;
1192         }
1193     }
1194
1195
1196     /**
1197      * Steps back, restoring the previous character read() to the input buffer
1198      * @ignore
1199      */
1200     protected function unread($c)
1201     {
1202         # FIXME: deal with unreading new lines
1203         $this->column -= mb_strlen($c, "UTF-8");
1204         $this->data = $c . $this->data;
1205     }
1206
1207     /** @ignore */
1208     protected function createBNode()
1209     {
1210         return array(
1211             'type' => 'bnode',
1212             'value' => $this->graph->newBNodeId()
1213         );
1214     }
1215
1216     /**
1217      * Returns true if $c is a whitespace character
1218      * @ignore
1219      */
1220     public static function isWhitespace($c)
1221     {
1222         // Whitespace character are space, tab, newline and carriage return:
1223         return $c == "\x20" || $c == "\x09" || $c == "\x0A" || $c == "\x0D";
1224     }
1225
1226     /** @ignore */
1227     public static function isPrefixStartChar($c)
1228     {
1229         $o = ord($c);
1230         return
1231             $o >= 0x41   && $o <= 0x5a ||     # A-Z
1232             $o >= 0x61   && $o <= 0x7a ||     # a-z
1233             $o >= 0x00C0 && $o <= 0x00D6 ||
1234             $o >= 0x00D8 && $o <= 0x00F6 ||
1235             $o >= 0x00F8 && $o <= 0x02FF ||
1236             $o >= 0x0370 && $o <= 0x037D ||
1237             $o >= 0x037F && $o <= 0x1FFF ||
1238             $o >= 0x200C && $o <= 0x200D ||
1239             $o >= 0x2070 && $o <= 0x218F ||
1240             $o >= 0x2C00 && $o <= 0x2FEF ||
1241             $o >= 0x3001 && $o <= 0xD7FF ||
1242             $o >= 0xF900 && $o <= 0xFDCF ||
1243             $o >= 0xFDF0 && $o <= 0xFFFD ||
1244             $o >= 0x10000 && $o <= 0xEFFFF;
1245     }
1246
1247     /** @ignore */
1248     public static function isNameStartChar($c)
1249     {
1250         return
1251             $c == '\\' ||
1252             $c == '_' ||
1253             $c == ':' ||
1254             $c == '%' ||
1255             ctype_digit($c) ||
1256             self::isPrefixStartChar($c);
1257     }
1258
1259     /** @ignore */
1260     public static function isNameChar($c)
1261     {
1262         $o = ord($c);
1263         return
1264             self::isNameStartChar($c) ||
1265             $o >= 0x30 && $o <= 0x39 ||     # 0-9
1266             $c == '-' ||
1267             $o == 0x00B7 ||
1268             $o >= 0x0300 && $o <= 0x036F ||
1269             $o >= 0x203F && $o <= 0x2040;
1270     }
1271
1272     /** @ignore */
1273     private static $localEscapedChars = array(
1274         '_', '~', '.', '-', '!', '$', '&', '\'', '(', ')',
1275         '*', '+', ',', ';', '=', '/', '?', '#', '@', '%'
1276     );
1277
1278     /** @ignore */
1279     public static function isLocalEscapedChar($c)
1280     {
1281         return in_array($c, self::$localEscapedChars);
1282     }
1283
1284     /** @ignore */
1285     public static function isPrefixChar($c)
1286     {
1287         $o = ord($c);
1288         return
1289             $c == '_' ||
1290             $o >= 0x30 && $o <= 0x39 ||     # 0-9
1291             self::isPrefixStartChar($c) ||
1292             $c == '-' ||
1293             $o == 0x00B7 ||
1294             $c >= 0x0300 && $c <= 0x036F ||
1295             $c >= 0x203F && $c <= 0x2040;
1296     }
1297
1298     /** @ignore */
1299     public static function isLanguageStartChar($c)
1300     {
1301         $o = ord($c);
1302         return
1303             $o >= 0x41 && $o <= 0x5a ||   # A-Z
1304             $o >= 0x61 && $o <= 0x7a;     # a-z
1305     }
1306
1307     /** @ignore */
1308     public static function isLanguageChar($c)
1309     {
1310         $o = ord($c);
1311         return
1312             $o >= 0x41 && $o <= 0x5a ||   # A-Z
1313             $o >= 0x61 && $o <= 0x7a ||   # a-z
1314             $o >= 0x30 && $o <= 0x39 ||   # 0-9
1315             $c == '-';
1316     }
1317 }