1<?php
2
3/**
4 * EasyRdf
5 *
6 * LICENSE
7 *
8 * Copyright (c) 2009-2013 Nicholas J Humfrey.  All rights reserved.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions are met:
12 * 1. Redistributions of source code must retain the above copyright
13 *    notice, this list of conditions and the following disclaimer.
14 * 2. Redistributions in binary form must reproduce the above copyright notice,
15 *    this list of conditions and the following disclaimer in the documentation
16 *    and/or other materials provided with the distribution.
17 * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
18 *    promote products derived from this software without specific prior
19 *    written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 *
33 * @package    EasyRdf
34 * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
35 * @license    http://www.opensource.org/licenses/bsd-license.php
36 */
37
38/**
39 * Container for collection of EasyRdf_Resources.
40 *
41 * @package    EasyRdf
42 * @copyright  Copyright (c) 2009-2013 Nicholas J Humfrey
43 * @license    http://www.opensource.org/licenses/bsd-license.php
44 */
45class EasyRdf_Graph
46{
47    /** The URI of the graph */
48    private $uri = null;
49    private $parsedUri = null;
50
51    /** Array of resources contained in the graph */
52    private $resources = array();
53
54    private $index = array();
55    private $revIndex = array();
56
57    /** Counter for the number of bnodes */
58    private $bNodeCount = 0;
59
60    /** Array of URLs that have been loaded into the graph */
61    private $loaded = array();
62
63    private $maxRedirects = 10;
64
65
66    /**
67     * Constructor
68     *
69     * If no URI is given then an unnamed graph is created.
70     *
71     * The $data parameter is optional and will be parsed into
72     * the graph if given.
73     *
74     * The data format is optional and should be specified if it
75     * can't be guessed by EasyRdf.
76     *
77     * @param  string  $uri     The URI of the graph
78     * @param  string  $data    Data for the graph
79     * @param  string  $format  The document type of the data (e.g. rdfxml)
80     * @return object EasyRdf_Graph
81     */
82    public function __construct($uri = null, $data = null, $format = null)
83    {
84        $this->checkResourceParam($uri, true);
85
86        if ($uri) {
87            $this->uri = $uri;
88            $this->parsedUri = new EasyRdf_ParsedUri($uri);
89            if ($data) {
90                $this->parse($data, $format, $this->uri);
91            }
92        }
93    }
94
95    /**
96     * Create a new graph and load RDF data from a URI into it
97     *
98     * This static function is shorthand for:
99     *     $graph = new EasyRdf_Graph($uri);
100     *     $graph->load($uri, $format);
101     *
102     * The document type is optional but should be specified if it
103     * can't be guessed or got from the HTTP headers.
104     *
105     * @param  string  $uri     The URI of the data to load
106     * @param  string  $format  Optional format of the data (eg. rdfxml)
107     * @return object EasyRdf_Graph    The new the graph object
108     */
109    public static function newAndLoad($uri, $format = null)
110    {
111        $graph = new self($uri);
112        $graph->load($uri, $format);
113        return $graph;
114    }
115
116    /** Get or create a resource stored in a graph
117     *
118     * If the resource did not previously exist, then a new resource will
119     * be created. If you provide an RDF type and that type is registered
120     * with the EasyRdf_TypeMapper, then the resource will be an instance
121     * of the class registered.
122     *
123     * If URI is null, then the URI of the graph is used.
124     *
125     * @param  string  $uri    The URI of the resource
126     * @param  mixed   $types  RDF type of a new resource (e.g. foaf:Person)
127     * @return object EasyRdf_Resource
128     */
129    public function resource($uri = null, $types = array())
130    {
131        $this->checkResourceParam($uri, true);
132        if (!$uri) {
133            throw new InvalidArgumentException(
134                '$uri is null and EasyRdf_Graph object has no URI either.'
135            );
136        }
137
138        // Resolve relative URIs
139        if ($this->parsedUri) {
140            $uri = $this->parsedUri->resolve($uri)->toString();
141        }
142
143        // Add the types
144        $this->addType($uri, $types);
145
146        // Create resource object if it doesn't already exist
147        if (!isset($this->resources[$uri])) {
148            $resClass = $this->classForResource($uri);
149            $this->resources[$uri] = new $resClass($uri, $this);
150        }
151
152        return $this->resources[$uri];
153    }
154
155    /** Work out the class to instantiate a resource as
156     *  @ignore
157     */
158    protected function classForResource($uri)
159    {
160        $rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
161        if (isset($this->index[$uri][$rdfType])) {
162            foreach ($this->index[$uri][$rdfType] as $type) {
163                if ($type['type'] == 'uri' or $type['type'] == 'bnode') {
164                    $class = EasyRdf_TypeMapper::get($type['value']);
165                    if ($class != null) {
166                        return $class;
167                    }
168                }
169            }
170        }
171
172        // Parsers don't typically add a rdf:type to rdf:List, so we have to
173        // do a bit of 'inference' here using properties.
174        if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or
175            isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or
176            isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'])
177        ) {
178            return 'EasyRdf_Collection';
179        }
180        return 'EasyRdf_Resource';
181    }
182
183    /**
184     * Create a new blank node in the graph and return it.
185     *
186     * If you provide an RDF type and that type is registered
187     * with the EasyRdf_TypeMapper, then the resource will be an instance
188     * of the class registered.
189     *
190     * @param  mixed  $types  RDF type of a new blank node (e.g. foaf:Person)
191     * @return object EasyRdf_Resource The new blank node
192     */
193    public function newBNode($types = array())
194    {
195        return $this->resource($this->newBNodeId(), $types);
196    }
197
198    /**
199     * Create a new unique blank node identifier and return it.
200     *
201     * @return string The new blank node identifier (e.g. _:genid1)
202     */
203    public function newBNodeId()
204    {
205        return "_:genid".(++$this->bNodeCount);
206    }
207
208    /**
209     * Parse some RDF data into the graph object.
210     *
211     * @param  string  $data    Data to parse for the graph
212     * @param  string  $format  Optional format of the data
213     * @param  string  $uri     The URI of the data to load
214     * @return integer          The number of triples added to the graph
215     */
216    public function parse($data, $format = null, $uri = null)
217    {
218        $this->checkResourceParam($uri, true);
219
220        if (empty($format) or $format == 'guess') {
221            // Guess the format if it is Unknown
222            $format = EasyRdf_Format::guessFormat($data, $uri);
223        } else {
224            $format = EasyRdf_Format::getFormat($format);
225        }
226
227        if (!$format) {
228            throw new EasyRdf_Exception(
229                "Unable to parse data of an unknown format."
230            );
231        }
232
233        $parser = $format->newParser();
234        return $parser->parse($this, $data, $format, $uri);
235    }
236
237    /**
238     * Parse a file containing RDF data into the graph object.
239     *
240     * @param  string  $filename The path of the file to load
241     * @param  string  $format   Optional format of the file
242     * @param  string  $uri      The URI of the file to load
243     * @return integer           The number of triples added to the graph
244     */
245    public function parseFile($filename, $format = null, $uri = null)
246    {
247        if ($uri === null) {
248            $uri = "file://$filename";
249        }
250
251        return $this->parse(
252            file_get_contents($filename),
253            $format,
254            $uri
255        );
256    }
257
258    /**
259     * Load RDF data into the graph from a URI.
260     *
261     * If no URI is given, then the URI of the graph will be used.
262     *
263     * The document type is optional but should be specified if it
264     * can't be guessed or got from the HTTP headers.
265     *
266     * @param  string  $uri     The URI of the data to load
267     * @param  string  $format  Optional format of the data (eg. rdfxml)
268     * @return integer          The number of triples added to the graph
269     */
270    public function load($uri = null, $format = null)
271    {
272        $this->checkResourceParam($uri, true);
273
274        if (!$uri) {
275            throw new EasyRdf_Exception(
276                "No URI given to load() and the graph does not have a URI."
277            );
278        }
279
280        // Setup the HTTP client
281        $client = EasyRdf_Http::getDefaultHttpClient();
282        $client->resetParameters(true);
283        $client->setConfig(array('maxredirects' => 0));
284        $client->setMethod('GET');
285        $client->setHeaders('Accept', EasyRdf_Format::getHttpAcceptHeader());
286
287        $requestUrl = $uri;
288        $response = null;
289        $redirectCounter = 0;
290        do {
291            // Have we already loaded it into the graph?
292            $requestUrl = EasyRdf_Utils::removeFragmentFromUri($requestUrl);
293            if (in_array($requestUrl, $this->loaded)) {
294                return 0;
295            }
296
297            // Make the HTTP request
298            $client->setHeaders('host', null);
299            $client->setUri($requestUrl);
300            $response = $client->request();
301
302            // Add the URL to the list of URLs loaded
303            $this->loaded[] = $requestUrl;
304
305            if ($response->isRedirect() and $location = $response->getHeader('location')) {
306                // Avoid problems with buggy servers that add whitespace
307                $location = trim($location);
308
309                // Some servers return relative URLs in the location header
310                // resolve it in relation to previous request
311                $baseUri = new EasyRdf_ParsedUri($requestUrl);
312                $requestUrl = $baseUri->resolve($location)->toString();
313                $requestUrl = EasyRdf_Utils::removeFragmentFromUri($requestUrl);
314
315                // If it is a 303 then drop the parameters
316                if ($response->getStatus() == 303) {
317                    $client->resetParameters();
318                }
319
320                ++$redirectCounter;
321            } elseif ($response->isSuccessful()) {
322                // If we didn't get any location, stop redirecting
323                break;
324            } else {
325                throw new EasyRdf_Http_Exception(
326                    "HTTP request for {$requestUrl} failed: ".$response->getMessage(),
327                    $response->getStatus(),
328                    null,
329                    $response->getBody()
330                );
331            }
332        } while ($redirectCounter < $this->maxRedirects);
333
334        if (!$format or $format == 'guess') {
335            list($format, $params) = EasyRdf_Utils::parseMimeType(
336                $response->getHeader('Content-Type')
337            );
338        }
339
340        // Parse the data
341        return $this->parse($response->getBody(), $format, $uri);
342    }
343
344    /** Get an associative array of all the resources stored in the graph.
345     *  The keys of the array is the URI of the EasyRdf_Resource.
346     *
347     * @return array Array of EasyRdf_Resource
348     */
349    public function resources()
350    {
351        foreach ($this->index as $subject => $properties) {
352            if (!isset($this->resources[$subject])) {
353                $this->resource($subject);
354            }
355        }
356
357        foreach ($this->revIndex as $object => $properties) {
358            if (!isset($this->resources[$object])) {
359                $this->resource($object);
360            }
361        }
362
363        return $this->resources;
364    }
365
366    /** Get an arry of resources matching a certain property and optional value.
367     *
368     * For example this routine could be used as a way of getting
369     * everyone who has name:
370     * $people = $graph->resourcesMatching('foaf:name')
371     *
372     * Or everyone who is male:
373     * $people = $graph->resourcesMatching('foaf:gender', 'male');
374     *
375     * Or all homepages:
376     * $people = $graph->resourcesMatching('^foaf:homepage');
377     *
378     * @param  string  $property   The property to check.
379     * @param  mixed   $value      Optional, the value of the propery to check for.
380     * @return array   Array of EasyRdf_Resource
381     */
382    public function resourcesMatching($property, $value = null)
383    {
384        $this->checkSinglePropertyParam($property, $inverse);
385        $this->checkValueParam($value);
386
387        // Use the reverse index if it is an inverse property
388        if ($inverse) {
389            $index = &$this->revIndex;
390        } else {
391            $index = &$this->index;
392        }
393
394        $matched = array();
395        foreach ($index as $subject => $props) {
396            if (isset($index[$subject][$property])) {
397                if (isset($value)) {
398                    foreach ($this->index[$subject][$property] as $v) {
399                        if ($v['type'] == $value['type'] and
400                            $v['value'] == $value['value']) {
401                            $matched[] = $this->resource($subject);
402                            break;
403                        }
404                    }
405                } else {
406                    $matched[] = $this->resource($subject);
407                }
408            }
409        }
410        return $matched;
411    }
412
413    /** Get the URI of the graph
414     *
415     * @return string The URI of the graph
416     */
417    public function getUri()
418    {
419        return $this->uri;
420    }
421
422    /** Check that a URI/resource parameter is valid, and convert it to a string
423     *  @ignore
424     */
425    protected function checkResourceParam(&$resource, $allowNull = false)
426    {
427        if ($allowNull == true) {
428            if ($resource === null) {
429                if ($this->uri) {
430                    $resource = $this->uri;
431                } else {
432                    return;
433                }
434            }
435        } elseif ($resource === null) {
436            throw new InvalidArgumentException(
437                "\$resource should be either IRI, blank-node identifier or EasyRdf_Resource. got null"
438            );
439        }
440
441        if (is_object($resource) and $resource instanceof EasyRdf_Resource) {
442            $resource = $resource->getUri();
443        } elseif (is_object($resource) and $resource instanceof EasyRdf_ParsedUri) {
444            $resource = strval($resource);
445        } elseif (is_string($resource)) {
446            if ($resource == '') {
447                throw new InvalidArgumentException(
448                    "\$resource should be either IRI, blank-node identifier or EasyRdf_Resource. got empty string"
449                );
450            } elseif (preg_match("|^<(.+)>$|", $resource, $matches)) {
451                $resource = $matches[1];
452            } else {
453                $resource = EasyRdf_Namespace::expand($resource);
454            }
455        } else {
456            throw new InvalidArgumentException(
457                "\$resource should be either IRI, blank-node identifier or EasyRdf_Resource"
458            );
459        }
460    }
461
462    /** Check that a single URI/property parameter (not a property path)
463     *  is valid, and expand it if required
464     *  @ignore
465     */
466    protected function checkSinglePropertyParam(&$property, &$inverse)
467    {
468        if (is_object($property) and $property instanceof EasyRdf_Resource) {
469            $property = $property->getUri();
470        } elseif (is_object($property) and $property instanceof EasyRdf_ParsedUri) {
471            $property = strval($property);
472        } elseif (is_string($property)) {
473            if ($property == '') {
474                throw new InvalidArgumentException(
475                    "\$property cannot be an empty string"
476                );
477            } elseif (substr($property, 0, 1) == '^') {
478                $inverse = true;
479                $property = EasyRdf_Namespace::expand(substr($property, 1));
480            } elseif (substr($property, 0, 2) == '_:') {
481                throw new InvalidArgumentException(
482                    "\$property cannot be a blank node"
483                );
484            } else {
485                $inverse = false;
486                $property = EasyRdf_Namespace::expand($property);
487            }
488        }
489
490        if ($property === null or !is_string($property)) {
491            throw new InvalidArgumentException(
492                "\$property should be a string or EasyRdf_Resource and cannot be null"
493            );
494        }
495    }
496
497    /** Check that a value parameter is valid, and convert it to an associative array if needed
498     *  @ignore
499     */
500    protected function checkValueParam(&$value)
501    {
502        if (isset($value)) {
503            if (is_object($value)) {
504                if (!method_exists($value, 'toRdfPhp')) {
505                    // Convert to a literal object
506                    $value = EasyRdf_Literal::create($value);
507                }
508                $value = $value->toRdfPhp();
509            } elseif (is_array($value)) {
510                if (!isset($value['type'])) {
511                    throw new InvalidArgumentException(
512                        "\$value is missing a 'type' key"
513                    );
514                }
515
516                if (!isset($value['value'])) {
517                    throw new InvalidArgumentException(
518                        "\$value is missing a 'value' key"
519                    );
520                }
521
522                // Fix ordering and remove unknown keys
523                $value = array(
524                    'type' => strval($value['type']),
525                    'value' => strval($value['value']),
526                    'lang' => isset($value['lang']) ? strval($value['lang']) : null,
527                    'datatype' => isset($value['datatype']) ? strval($value['datatype']) : null
528                );
529            } else {
530                $value = array(
531                    'type' => 'literal',
532                    'value' => strval($value),
533                    'datatype' => EasyRdf_Literal::getDatatypeForValue($value)
534                );
535            }
536            if (!in_array($value['type'], array('uri', 'bnode', 'literal'), true)) {
537                throw new InvalidArgumentException(
538                    "\$value does not have a valid type (".$value['type'].")"
539                );
540            }
541            if (empty($value['datatype'])) {
542                unset($value['datatype']);
543            }
544            if (empty($value['lang'])) {
545                unset($value['lang']);
546            }
547            if (isset($value['lang']) and isset($value['datatype'])) {
548                throw new InvalidArgumentException(
549                    "\$value cannot have both and language and a datatype"
550                );
551            }
552        }
553    }
554
555    /** Get a single value for a property of a resource
556     *
557     * If multiple values are set for a property then the value returned
558     * may be arbitrary.
559     *
560     * If $property is an array, then the first item in the array that matches
561     * a property that exists is returned.
562     *
563     * This method will return null if the property does not exist.
564     *
565     * @param  string    $resource       The URI of the resource (e.g. http://example.com/joe#me)
566     * @param  string    $propertyPath   A valid property path
567     * @param  string    $type           The type of value to filter by (e.g. literal or resource)
568     * @param  string    $lang           The language to filter by (e.g. en)
569     * @return mixed                     A value associated with the property
570     */
571    public function get($resource, $propertyPath, $type = null, $lang = null)
572    {
573        $this->checkResourceParam($resource);
574
575        if (is_object($propertyPath) and $propertyPath instanceof EasyRdf_Resource) {
576            return $this->getSingleProperty($resource, $propertyPath->getUri(), $type, $lang);
577        } elseif (is_string($propertyPath) and preg_match('|^(\^?)<(.+)>|', $propertyPath, $matches)) {
578            return $this->getSingleProperty($resource, "$matches[1]$matches[2]", $type, $lang);
579        } elseif ($propertyPath === null or !is_string($propertyPath)) {
580            throw new InvalidArgumentException(
581                "\$propertyPath should be a string or EasyRdf_Resource and cannot be null"
582            );
583        } elseif ($propertyPath === '') {
584            throw new InvalidArgumentException(
585                "\$propertyPath cannot be an empty string"
586            );
587        }
588
589        // Loop through each component in the path
590        foreach (explode('/', $propertyPath) as $part) {
591            // Stop if we come to a literal
592            if ($resource instanceof EasyRdf_Literal) {
593                return null;
594            }
595
596            // Try each of the alternative paths
597            foreach (explode('|', $part) as $p) {
598                $res = $this->getSingleProperty($resource, $p, $type, $lang);
599                if ($res) {
600                    break;
601                }
602            }
603
604            // Stop if nothing was found
605            $resource = $res;
606            if (!$resource) {
607                break;
608            }
609        }
610
611        return $resource;
612    }
613
614    /** Get a single value for a property of a resource
615     *
616     * @param  string    $resource The URI of the resource (e.g. http://example.com/joe#me)
617     * @param  string    $property The name of the property (e.g. foaf:name)
618     * @param  string    $type     The type of value to filter by (e.g. literal or resource)
619     * @param  string    $lang     The language to filter by (e.g. en)
620     * @return mixed               A value associated with the property
621     *
622     * @ignore
623     */
624    protected function getSingleProperty($resource, $property, $type = null, $lang = null)
625    {
626        $this->checkResourceParam($resource);
627        $this->checkSinglePropertyParam($property, $inverse);
628
629        // Get an array of values for the property
630        $values = $this->propertyValuesArray($resource, $property, $inverse);
631        if (!isset($values)) {
632            return null;
633        }
634
635        // Filter the results
636        $result = null;
637        if ($type) {
638            foreach ($values as $value) {
639                if ($type == 'literal' and $value['type'] == 'literal') {
640                    if ($lang == null or (isset($value['lang']) and $value['lang'] == $lang)) {
641                        $result = $value;
642                        break;
643                    }
644                } elseif ($type == 'resource') {
645                    if ($value['type'] == 'uri' or $value['type'] == 'bnode') {
646                        $result = $value;
647                        break;
648                    }
649                }
650            }
651        } else {
652            $result = $values[0];
653        }
654
655        // Convert the internal data structure into a PHP object
656        return $this->arrayToObject($result);
657    }
658
659    /** Get a single literal value for a property of a resource
660     *
661     * If multiple values are set for a property then the value returned
662     * may be arbitrary.
663     *
664     * This method will return null if there is not literal value for the
665     * property.
666     *
667     * @param  string       $resource The URI of the resource (e.g. http://example.com/joe#me)
668     * @param  string|array $property The name of the property (e.g. foaf:name)
669     * @param  string       $lang     The language to filter by (e.g. en)
670     * @return object EasyRdf_Literal Literal value associated with the property
671     */
672    public function getLiteral($resource, $property, $lang = null)
673    {
674        return $this->get($resource, $property, 'literal', $lang);
675    }
676
677    /** Get a single resource value for a property of a resource
678     *
679     * If multiple values are set for a property then the value returned
680     * may be arbitrary.
681     *
682     * This method will return null if there is not resource for the
683     * property.
684     *
685     * @param  string       $resource The URI of the resource (e.g. http://example.com/joe#me)
686     * @param  string|array $property The name of the property (e.g. foaf:name)
687     * @return object EasyRdf_Resource Resource associated with the property
688     */
689    public function getResource($resource, $property)
690    {
691        return $this->get($resource, $property, 'resource');
692    }
693
694    /** Return all the values for a particular property of a resource
695     *  @ignore
696     */
697    protected function propertyValuesArray($resource, $property, $inverse = false)
698    {
699        // Is an inverse property being requested?
700        if ($inverse) {
701            if (isset($this->revIndex[$resource])) {
702                $properties = &$this->revIndex[$resource];
703            }
704        } else {
705            if (isset($this->index[$resource])) {
706                $properties = &$this->index[$resource];
707            }
708        }
709
710        if (isset($properties[$property])) {
711            return $properties[$property];
712        } else {
713            return null;
714        }
715    }
716
717    /** Get an EasyRdf_Resource or EasyRdf_Literal object from an associative array.
718     *  @ignore
719     */
720    protected function arrayToObject($data)
721    {
722        if ($data) {
723            if ($data['type'] == 'uri' or $data['type'] == 'bnode') {
724                return $this->resource($data['value']);
725            } else {
726                return EasyRdf_Literal::create($data);
727            }
728        } else {
729            return null;
730        }
731    }
732
733    /** Get all values for a property path
734     *
735     * This method will return an empty array if the property does not exist.
736     *
737     * @param  string  $resource      The URI of the resource (e.g. http://example.com/joe#me)
738     * @param  string  $propertyPath  A valid property path
739     * @param  string  $type          The type of value to filter by (e.g. literal)
740     * @param  string  $lang          The language to filter by (e.g. en)
741     * @return array                  An array of values associated with the property
742     */
743    public function all($resource, $propertyPath, $type = null, $lang = null)
744    {
745        $this->checkResourceParam($resource);
746
747        if (is_object($propertyPath) and $propertyPath instanceof EasyRdf_Resource) {
748            return $this->allForSingleProperty($resource, $propertyPath->getUri(), $type, $lang);
749        } elseif (is_string($propertyPath) and preg_match('|^(\^?)<(.+)>|', $propertyPath, $matches)) {
750            return $this->allForSingleProperty($resource, "$matches[1]$matches[2]", $type, $lang);
751        } elseif ($propertyPath === null or !is_string($propertyPath)) {
752            throw new InvalidArgumentException(
753                "\$propertyPath should be a string or EasyRdf_Resource and cannot be null"
754            );
755        } elseif ($propertyPath === '') {
756            throw new InvalidArgumentException(
757                "\$propertyPath cannot be an empty string"
758            );
759        }
760
761        $objects = array($resource);
762
763        // Loop through each component in the path
764        foreach (explode('/', $propertyPath) as $part) {
765
766            $results = array();
767            foreach (explode('|', $part) as $p) {
768                foreach ($objects as $o) {
769                    // Ignore literals found earlier in path
770                    if ($o instanceof EasyRdf_Literal) {
771                        continue;
772                    }
773
774                    $results = array_merge(
775                        $results,
776                        $this->allForSingleProperty($o, $p, $type, $lang)
777                    );
778                }
779            }
780
781            // Stop if we don't have anything
782            if (empty($objects)) {
783                break;
784            }
785
786            // Use the results as the input to the next iteration
787            $objects = $results;
788        }
789
790        return $results;
791    }
792
793    /** Get all values for a single property of a resource
794     *
795     * @param  string  $resource The URI of the resource (e.g. http://example.com/joe#me)
796     * @param  string  $property The name of the property (e.g. foaf:name)
797     * @param  string  $type     The type of value to filter by (e.g. literal)
798     * @param  string  $lang     The language to filter by (e.g. en)
799     * @return array             An array of values associated with the property
800     *
801     * @ignore
802     */
803    protected function allForSingleProperty($resource, $property, $type = null, $lang = null)
804    {
805        $this->checkResourceParam($resource);
806        $this->checkSinglePropertyParam($property, $inverse);
807
808        // Get an array of values for the property
809        $values = $this->propertyValuesArray($resource, $property, $inverse);
810        if (!isset($values)) {
811            return array();
812        }
813
814        $objects = array();
815        if ($type) {
816            foreach ($values as $value) {
817                if ($type == 'literal' and $value['type'] == 'literal') {
818                    if ($lang == null or (isset($value['lang']) and $value['lang'] == $lang)) {
819                        $objects[] = $this->arrayToObject($value);
820                    }
821                } elseif ($type == 'resource') {
822                    if ($value['type'] == 'uri' or $value['type'] == 'bnode') {
823                        $objects[] = $this->arrayToObject($value);
824                    }
825                }
826            }
827        } else {
828            foreach ($values as $value) {
829                $objects[] = $this->arrayToObject($value);
830            }
831        }
832        return $objects;
833    }
834
835    /** Get all literal values for a property of a resource
836     *
837     * This method will return an empty array if the resource does not
838     * has any literal values for that property.
839     *
840     * @param  string  $resource The URI of the resource (e.g. http://example.com/joe#me)
841     * @param  string  $property The name of the property (e.g. foaf:name)
842     * @param  string  $lang     The language to filter by (e.g. en)
843     * @return array             An array of values associated with the property
844     */
845    public function allLiterals($resource, $property, $lang = null)
846    {
847        return $this->all($resource, $property, 'literal', $lang);
848    }
849
850    /** Get all resources for a property of a resource
851     *
852     * This method will return an empty array if the resource does not
853     * has any resources for that property.
854     *
855     * @param  string  $resource The URI of the resource (e.g. http://example.com/joe#me)
856     * @param  string  $property The name of the property (e.g. foaf:name)
857     * @return array             An array of values associated with the property
858     */
859    public function allResources($resource, $property)
860    {
861        return $this->all($resource, $property, 'resource');
862    }
863
864    /** Get all the resources in the graph of a certain type
865     *
866     * If no resources of the type are available and empty
867     * array is returned.
868     *
869     * @param  string  $type   The type of the resource (e.g. foaf:Person)
870     * @return array The array of resources
871     */
872    public function allOfType($type)
873    {
874        return $this->all($type, '^rdf:type');
875    }
876
877    /** Count the number of values for a property of a resource
878     *
879     * @param  string  $resource The URI of the resource (e.g. http://example.com/joe#me)
880     * @param  string  $property The name of the property (e.g. foaf:name)
881     * @param  string  $type     The type of value to filter by (e.g. literal)
882     * @param  string  $lang     The language to filter by (e.g. en)
883     * @return integer           The number of values for this property
884     */
885    public function countValues($resource, $property, $type = null, $lang = null)
886    {
887        return count($this->all($resource, $property, $type, $lang));
888    }
889
890    /** Concatenate all values for a property of a resource into a string.
891     *
892     * The default is to join the values together with a space character.
893     * This method will return an empty string if the property does not exist.
894     *
895     * @param  mixed   $resource The resource to get the property on
896     * @param  string  $property The name of the property (e.g. foaf:name)
897     * @param  string  $glue     The string to glue the values together with.
898     * @param  string  $lang     The language to filter by (e.g. en)
899     * @return string            Concatenation of all the values.
900     */
901    public function join($resource, $property, $glue = ' ', $lang = null)
902    {
903        return join($glue, $this->all($resource, $property, 'literal', $lang));
904    }
905
906    /** Add data to the graph
907     *
908     * The resource can either be a resource or the URI of a resource.
909     *
910     * Example:
911     *   $graph->add("http://www.example.com", 'dc:title', 'Title of Page');
912     *
913     * @param  mixed $resource   The resource to add data to
914     * @param  mixed $property   The property name
915     * @param  mixed $value      The new value for the property
916     * @return integer           The number of values added (1 or 0)
917     */
918    public function add($resource, $property, $value)
919    {
920        $this->checkResourceParam($resource);
921        $this->checkSinglePropertyParam($property, $inverse);
922        $this->checkValueParam($value);
923
924        // No value given?
925        if ($value === null) {
926            return 0;
927        }
928
929        // Check that the value doesn't already exist
930        if (isset($this->index[$resource][$property])) {
931            foreach ($this->index[$resource][$property] as $v) {
932                if ($v == $value) {
933                    return 0;
934                }
935            }
936        }
937        $this->index[$resource][$property][] = $value;
938
939        // Add to the reverse index if it is a resource
940        if ($value['type'] == 'uri' or $value['type'] == 'bnode') {
941            $uri = $value['value'];
942            $this->revIndex[$uri][$property][] = array(
943                'type' => substr($resource, 0, 2) == '_:' ? 'bnode' : 'uri',
944                'value' => $resource
945            );
946        }
947
948        // Success
949        return 1;
950    }
951
952    /** Add a literal value as a property of a resource
953     *
954     * The resource can either be a resource or the URI of a resource.
955     * The value can either be a single value or an array of values.
956     *
957     * Example:
958     *   $graph->add("http://www.example.com", 'dc:title', 'Title of Page');
959     *
960     * @param  mixed  $resource  The resource to add data to
961     * @param  mixed  $property  The property name
962     * @param  mixed  $value     The value or values for the property
963     * @param  string $lang      The language of the literal
964     * @return integer           The number of values added
965     */
966    public function addLiteral($resource, $property, $value, $lang = null)
967    {
968        $this->checkResourceParam($resource);
969        $this->checkSinglePropertyParam($property, $inverse);
970
971        if (is_array($value)) {
972            $added = 0;
973            foreach ($value as $v) {
974                $added += $this->addLiteral($resource, $property, $v, $lang);
975            }
976            return $added;
977        } elseif (!is_object($value) or !$value instanceof EasyRdf_Literal) {
978            $value = EasyRdf_Literal::create($value, $lang);
979        }
980        return $this->add($resource, $property, $value);
981    }
982
983    /** Add a resource as a property of another resource
984     *
985     * The resource can either be a resource or the URI of a resource.
986     *
987     * Example:
988     *   $graph->add("http://example.com/bob", 'foaf:knows', 'http://example.com/alice');
989     *
990     * @param  mixed $resource   The resource to add data to
991     * @param  mixed $property   The property name
992     * @param  mixed $resource2  The resource to be value of the property
993     * @return integer           The number of values added
994     */
995    public function addResource($resource, $property, $resource2)
996    {
997        $this->checkResourceParam($resource);
998        $this->checkSinglePropertyParam($property, $inverse);
999        $this->checkResourceParam($resource2);
1000
1001        return $this->add(
1002            $resource,
1003            $property,
1004            array(
1005                'type' => substr($resource2, 0, 2) == '_:' ? 'bnode' : 'uri',
1006                'value' => $resource2
1007            )
1008        );
1009    }
1010
1011    /** Set a value for a property
1012     *
1013     * The new value will replace the existing values for the property.
1014     *
1015     * @param  string  $resource The resource to set the property on
1016     * @param  string  $property The name of the property (e.g. foaf:name)
1017     * @param  mixed   $value    The value for the property
1018     * @return integer           The number of values added (1 or 0)
1019     */
1020    public function set($resource, $property, $value)
1021    {
1022        $this->checkResourceParam($resource);
1023        $this->checkSinglePropertyParam($property, $inverse);
1024        $this->checkValueParam($value);
1025
1026        // Delete the old values
1027        $this->delete($resource, $property);
1028
1029        // Add the new values
1030        return $this->add($resource, $property, $value);
1031    }
1032
1033    /** Delete a property (or optionally just a specific value)
1034     *
1035     * @param  mixed   $resource The resource to delete the property from
1036     * @param  string  $property The name of the property (e.g. foaf:name)
1037     * @param  mixed   $value The value to delete (null to delete all values)
1038     * @return integer The number of values deleted
1039     */
1040    public function delete($resource, $property, $value = null)
1041    {
1042        $this->checkResourceParam($resource);
1043
1044        if (is_object($property) and $property instanceof EasyRdf_Resource) {
1045            return $this->deleteSingleProperty($resource, $property->getUri(), $value);
1046        } elseif (is_string($property) and preg_match('|^(\^?)<(.+)>|', $property, $matches)) {
1047            return $this->deleteSingleProperty($resource, "$matches[1]$matches[2]", $value);
1048        } elseif ($property === null or !is_string($property)) {
1049            throw new InvalidArgumentException(
1050                "\$property should be a string or EasyRdf_Resource and cannot be null"
1051            );
1052        } elseif ($property === '') {
1053            throw new InvalidArgumentException(
1054                "\$property cannot be an empty string"
1055            );
1056        }
1057
1058        // FIXME: finish implementing property paths for delete
1059        return $this->deleteSingleProperty($resource, $property, $value);
1060    }
1061
1062
1063    /** Delete a property (or optionally just a specific value)
1064     *
1065     * @param  mixed   $resource The resource to delete the property from
1066     * @param  string  $property The name of the property (e.g. foaf:name)
1067     * @param  mixed   $value The value to delete (null to delete all values)
1068     * @return integer The number of values deleted
1069     *
1070     * @ignore
1071     */
1072    public function deleteSingleProperty($resource, $property, $value = null)
1073    {
1074        $this->checkResourceParam($resource);
1075        $this->checkSinglePropertyParam($property, $inverse);
1076        $this->checkValueParam($value);
1077
1078        $count = 0;
1079        if (isset($this->index[$resource][$property])) {
1080            foreach ($this->index[$resource][$property] as $k => $v) {
1081                if (!$value or $v == $value) {
1082                    unset($this->index[$resource][$property][$k]);
1083                    $count++;
1084                    if ($v['type'] == 'uri' or $v['type'] == 'bnode') {
1085                        $this->deleteInverse($v['value'], $property, $resource);
1086                    }
1087                }
1088            }
1089
1090            // Clean up the indexes - remove empty properties and resources
1091            if ($count) {
1092                if (count($this->index[$resource][$property]) == 0) {
1093                    unset($this->index[$resource][$property]);
1094                }
1095                if (count($this->index[$resource]) == 0) {
1096                    unset($this->index[$resource]);
1097                }
1098            }
1099        }
1100
1101        return $count;
1102    }
1103
1104    /** Delete a resource from a property of another resource
1105     *
1106     * The resource can either be a resource or the URI of a resource.
1107     *
1108     * Example:
1109     *   $graph->delete("http://example.com/bob", 'foaf:knows', 'http://example.com/alice');
1110     *
1111     * @param  mixed $resource   The resource to delete data from
1112     * @param  mixed $property   The property name
1113     * @param  mixed $resource2  The resource value of the property to be deleted
1114     */
1115    public function deleteResource($resource, $property, $resource2)
1116    {
1117        $this->checkResourceParam($resource);
1118        $this->checkSinglePropertyParam($property, $inverse);
1119        $this->checkResourceParam($resource2);
1120
1121        return $this->delete(
1122            $resource,
1123            $property,
1124            array(
1125                'type' => substr($resource2, 0, 2) == '_:' ? 'bnode' : 'uri',
1126                'value' => $resource2
1127            )
1128        );
1129    }
1130
1131    /** Delete a literal value from a property of a resource
1132     *
1133     * Example:
1134     *   $graph->delete("http://www.example.com", 'dc:title', 'Title of Page');
1135     *
1136     * @param  mixed  $resource  The resource to add data to
1137     * @param  mixed  $property  The property name
1138     * @param  mixed  $value     The value of the property
1139     * @param  string $lang      The language of the literal
1140     */
1141    public function deleteLiteral($resource, $property, $value, $lang = null)
1142    {
1143        $this->checkResourceParam($resource);
1144        $this->checkSinglePropertyParam($property, $inverse);
1145        $this->checkValueParam($value);
1146
1147        if ($lang) {
1148            $value['lang'] = $lang;
1149        }
1150
1151        return $this->delete($resource, $property, $value);
1152    }
1153
1154    /** This function is for internal use only.
1155     *
1156     * Deletes an inverse property from a resource.
1157     *
1158     * @ignore
1159     */
1160    protected function deleteInverse($resource, $property, $value)
1161    {
1162        if (isset($this->revIndex[$resource])) {
1163            foreach ($this->revIndex[$resource][$property] as $k => $v) {
1164                if ($v['value'] === $value) {
1165                    unset($this->revIndex[$resource][$property][$k]);
1166                }
1167            }
1168            if (count($this->revIndex[$resource][$property]) == 0) {
1169                unset($this->revIndex[$resource][$property]);
1170            }
1171            if (count($this->revIndex[$resource]) == 0) {
1172                unset($this->revIndex[$resource]);
1173            }
1174        }
1175    }
1176
1177    /** Check if the graph contains any statements
1178     *
1179     * @return boolean True if the graph contains no statements
1180     */
1181    public function isEmpty()
1182    {
1183        return count($this->index) == 0;
1184    }
1185
1186    /** Get a list of all the shortened property names (qnames) for a resource.
1187     *
1188     * This method will return an empty array if the resource has no properties.
1189     *
1190     * @return array            Array of shortened URIs
1191     */
1192    public function properties($resource)
1193    {
1194        $this->checkResourceParam($resource);
1195
1196        $properties = array();
1197        if (isset($this->index[$resource])) {
1198            foreach ($this->index[$resource] as $property => $value) {
1199                $short = EasyRdf_Namespace::shorten($property);
1200                if ($short) {
1201                    $properties[] = $short;
1202                }
1203            }
1204        }
1205        return $properties;
1206    }
1207
1208    /** Get a list of the full URIs for the properties of a resource.
1209     *
1210     * This method will return an empty array if the resource has no properties.
1211     *
1212     * @return array            Array of full URIs
1213     */
1214    public function propertyUris($resource)
1215    {
1216        $this->checkResourceParam($resource);
1217
1218        if (isset($this->index[$resource])) {
1219            return array_keys($this->index[$resource]);
1220        } else {
1221            return array();
1222        }
1223    }
1224
1225    /** Get a list of the full URIs for the properties that point to a resource.
1226     *
1227     * @return array   Array of full property URIs
1228     */
1229    public function reversePropertyUris($resource)
1230    {
1231        $this->checkResourceParam($resource);
1232
1233        if (isset($this->revIndex[$resource])) {
1234            return array_keys($this->revIndex[$resource]);
1235        } else {
1236            return array();
1237        }
1238    }
1239
1240    /** Check to see if a property exists for a resource.
1241     *
1242     * This method will return true if the property exists.
1243     * If the value parameter is given, then it will only return true
1244     * if the value also exists for that property.
1245     *
1246     * By providing a value parameter you can use this function to check
1247     * to see if a triple exists in the graph.
1248     *
1249     * @param  mixed   $resource The resource to check
1250     * @param  string  $property The name of the property (e.g. foaf:name)
1251     * @param  mixed   $value    An optional value of the property
1252     * @return boolean           True if value the property exists.
1253     */
1254    public function hasProperty($resource, $property, $value = null)
1255    {
1256        $this->checkResourceParam($resource);
1257        $this->checkSinglePropertyParam($property, $inverse);
1258        $this->checkValueParam($value);
1259
1260        // Use the reverse index if it is an inverse property
1261        if ($inverse) {
1262            $index = &$this->revIndex;
1263        } else {
1264            $index = &$this->index;
1265        }
1266
1267        if (isset($index[$resource][$property])) {
1268            if (is_null($value)) {
1269                return true;
1270            } else {
1271                foreach ($index[$resource][$property] as $v) {
1272                    if ($v == $value) {
1273                        return true;
1274                    }
1275                }
1276            }
1277        }
1278
1279        return false;
1280    }
1281
1282    /** Serialise the graph into RDF
1283     *
1284     * The $format parameter can be an EasyRdf_Format object, a
1285     * format name, a mime type or a file extension.
1286     *
1287     * Example:
1288     *   $turtle = $graph->serialise('turtle');
1289     *
1290     * @param  mixed $format  The format to serialise to
1291     * @param  array $options Serialiser-specific options, for fine-tuning the output
1292     * @return mixed  The serialised graph
1293     */
1294    public function serialise($format, array $options = array())
1295    {
1296        if (!$format instanceof EasyRdf_Format) {
1297            $format = EasyRdf_Format::getFormat($format);
1298        }
1299        $serialiser = $format->newSerialiser();
1300        return $serialiser->serialise($this, $format->getName(), $options);
1301    }
1302
1303    /** Return a human readable view of all the resources in the graph
1304     *
1305     * This method is intended to be a debugging aid and will
1306     * return a pretty-print view of all the resources and their
1307     * properties.
1308     *
1309     * @param  string  $format  Either 'html' or 'text'
1310     * @return string
1311     */
1312    public function dump($format = 'html')
1313    {
1314        $result = '';
1315        if ($format == 'html') {
1316            $result .= "<div style='font-family:arial; font-weight: bold; padding:0.5em; ".
1317                   "color: black; background-color:lightgrey;border:dashed 1px grey;'>".
1318                   "Graph: ". $this->uri . "</div>\n";
1319        } else {
1320            $result .= "Graph: ". $this->uri . "\n";
1321        }
1322
1323        foreach ($this->index as $resource => $properties) {
1324            $result .= $this->dumpResource($resource, $format);
1325        }
1326        return $result;
1327    }
1328
1329    /** Return a human readable view of a resource and its properties
1330     *
1331     * This method is intended to be a debugging aid and will
1332     * print a resource and its properties.
1333     *
1334     * @param  mixed    $resource  The resource to dump
1335     * @param  string   $format    Either 'html' or 'text'
1336     * @return string
1337     */
1338    public function dumpResource($resource, $format = 'html')
1339    {
1340        $this->checkResourceParam($resource, true);
1341
1342        if (isset($this->index[$resource])) {
1343            $properties = $this->index[$resource];
1344        } else {
1345            return '';
1346        }
1347
1348        $plist = array();
1349        foreach ($properties as $property => $values) {
1350            $olist = array();
1351            foreach ($values as $value) {
1352                if ($value['type'] == 'literal') {
1353                    $olist []= EasyRdf_Utils::dumpLiteralValue($value, $format, 'black');
1354                } else {
1355                    $olist []= EasyRdf_Utils::dumpResourceValue($value['value'], $format, 'blue');
1356                }
1357            }
1358
1359            $pstr = EasyRdf_Namespace::shorten($property);
1360            if ($pstr == null) {
1361                $pstr = $property;
1362            }
1363            if ($format == 'html') {
1364                $plist []= "<span style='font-size:130%'>&rarr;</span> ".
1365                           "<span style='text-decoration:none;color:green'>".
1366                           htmlentities($pstr) . "</span> ".
1367                           "<span style='font-size:130%'>&rarr;</span> ".
1368                           join(", ", $olist);
1369            } else {
1370                $plist []= "  -> $pstr -> " . join(", ", $olist);
1371            }
1372        }
1373
1374        if ($format == 'html') {
1375            return "<div id='".htmlentities($resource, ENT_QUOTES)."' " .
1376                   "style='font-family:arial; padding:0.5em; ".
1377                   "background-color:lightgrey;border:dashed 1px grey;'>\n".
1378                   "<div>".EasyRdf_Utils::dumpResourceValue($resource, $format, 'blue')." ".
1379                   "<span style='font-size: 0.8em'>(".
1380                   $this->classForResource($resource).")</span></div>\n".
1381                   "<div style='padding-left: 3em'>\n".
1382                   "<div>".join("</div>\n<div>", $plist)."</div>".
1383                   "</div></div>\n";
1384        } else {
1385            return $resource." (".$this->classForResource($resource).")\n" .
1386                   join("\n", $plist) . "\n\n";
1387        }
1388    }
1389
1390    /** Get the resource type of the graph
1391     *
1392     * The type will be a shortened URI as a string.
1393     * If the graph has multiple types then the type returned
1394     * may be arbitrary.
1395     * This method will return null if the resource has no type.
1396     *
1397     * @return string A type assocated with the resource (e.g. foaf:Document)
1398     */
1399    public function type($resource = null)
1400    {
1401        $type = $this->typeAsResource($resource);
1402
1403        if ($type) {
1404            return EasyRdf_Namespace::shorten($type);
1405        }
1406
1407        return null;
1408    }
1409
1410    /** Get the resource type of the graph as a EasyRdf_Resource
1411     *
1412     * If the graph has multiple types then the type returned
1413     * may be arbitrary.
1414     * This method will return null if the resource has no type.
1415     *
1416     * @return object EasyRdf_Resource  A type assocated with the resource
1417     */
1418    public function typeAsResource($resource = null)
1419    {
1420        $this->checkResourceParam($resource, true);
1421
1422        if ($resource) {
1423            return $this->get($resource, 'rdf:type', 'resource');
1424        }
1425
1426        return null;
1427    }
1428
1429    /** Get a list of types for a resource
1430     *
1431     * The types will each be a shortened URI as a string.
1432     * This method will return an empty array if the resource has no types.
1433     *
1434     * If $resource is null, then it will get the types for the URI of the graph.
1435     *
1436     * @return array All types assocated with the resource (e.g. foaf:Person)
1437     */
1438    public function types($resource = null)
1439    {
1440        $resources = $this->typesAsResources($resource);
1441
1442        $types = array();
1443        foreach ($resources as $type) {
1444            $types[] = EasyRdf_Namespace::shorten($type);
1445        }
1446
1447        return $types;
1448    }
1449
1450    /**
1451     * Get the resource types of the graph as a EasyRdf_Resource
1452     *
1453     * @return EasyRdf_Resource[]
1454     */
1455    public function typesAsResources($resource = null)
1456    {
1457        $this->checkResourceParam($resource, true);
1458
1459        if ($resource) {
1460            return $this->all($resource, 'rdf:type', 'resource');
1461        }
1462
1463        return array();
1464    }
1465
1466    /** Check if a resource is of the specified type
1467     *
1468     * @param  string  $resource The resource to check the type of
1469     * @param  string  $type     The type to check (e.g. foaf:Person)
1470     * @return boolean           True if resource is of specified type
1471     */
1472    public function isA($resource, $type)
1473    {
1474        $this->checkResourceParam($resource, true);
1475
1476        $type = EasyRdf_Namespace::expand($type);
1477        foreach ($this->all($resource, 'rdf:type', 'resource') as $t) {
1478            if ($t->getUri() == $type) {
1479                return true;
1480            }
1481        }
1482        return false;
1483    }
1484
1485    /** Add one or more rdf:type properties to a resource
1486     *
1487     * @param  string  $resource The resource to add the type to
1488     * @param  string  $types    One or more types to add (e.g. foaf:Person)
1489     * @return integer           The number of types added
1490     */
1491    public function addType($resource, $types)
1492    {
1493        $this->checkResourceParam($resource, true);
1494
1495        if (!is_array($types)) {
1496            $types = array($types);
1497        }
1498
1499        $count = 0;
1500        foreach ($types as $type) {
1501            $type = EasyRdf_Namespace::expand($type);
1502            $count += $this->add($resource, 'rdf:type', array('type' => 'uri', 'value' => $type));
1503        }
1504
1505        return $count;
1506    }
1507
1508    /** Change the rdf:type property for a resource
1509     *
1510     * Note that if the resource object has already previously
1511     * been created, then the PHP class of the resource will not change.
1512     *
1513     * @param  string  $resource The resource to change the type of
1514     * @param  string  $type     The new type (e.g. foaf:Person)
1515     * @return integer           The number of types added
1516     */
1517    public function setType($resource, $type)
1518    {
1519        $this->checkResourceParam($resource, true);
1520
1521        $this->delete($resource, 'rdf:type');
1522        return $this->addType($resource, $type);
1523    }
1524
1525    /** Get a human readable label for a resource
1526     *
1527     * This method will check a number of properties for a resource
1528     * (in the order: skos:prefLabel, rdfs:label, foaf:name, dc:title)
1529     * and return an approriate first that is available. If no label
1530     * is available then it will return null.
1531     *
1532     * @return string A label for the resource.
1533     */
1534    public function label($resource = null, $lang = null)
1535    {
1536        $this->checkResourceParam($resource, true);
1537
1538        if ($resource) {
1539            return $this->get(
1540                $resource,
1541                'skos:prefLabel|rdfs:label|foaf:name|rss:title|dc:title|dc11:title',
1542                'literal',
1543                $lang
1544            );
1545        } else {
1546            return null;
1547        }
1548    }
1549
1550    /** Get the primary topic of the graph
1551     *
1552     * @return EasyRdf_Resource The primary topic of the document.
1553     */
1554    public function primaryTopic($resource = null)
1555    {
1556        $this->checkResourceParam($resource, true);
1557
1558        if ($resource) {
1559            return $this->get(
1560                $resource,
1561                'foaf:primaryTopic|^foaf:isPrimaryTopicOf',
1562                'resource'
1563            );
1564        } else {
1565            return null;
1566        }
1567    }
1568
1569    /** Returns the graph as a RDF/PHP associative array
1570     *
1571     * @return array The contents of the graph as an array.
1572     */
1573    public function toRdfPhp()
1574    {
1575        return $this->index;
1576    }
1577
1578    /** Calculates the number of triples in the graph
1579     *
1580     * @return integer The number of triples in the graph.
1581     */
1582    public function countTriples()
1583    {
1584        $count = 0;
1585        foreach ($this->index as $resource) {
1586            foreach ($resource as $property => $values) {
1587                $count += count($values);
1588            }
1589        }
1590        return $count;
1591    }
1592
1593    /** Magic method to return URI of resource when casted to string
1594     *
1595     * @return string The URI of the resource
1596     */
1597    public function __toString()
1598    {
1599        return $this->uri == null ? '' : $this->uri;
1600    }
1601
1602    /** Magic method to get a property of the graph
1603     *
1604     * Note that only properties in the default namespace can be accessed in this way.
1605     *
1606     * Example:
1607     *   $value = $graph->title;
1608     *
1609     * @see EasyRdf_Namespace::setDefault()
1610     * @param  string $name The name of the property
1611     * @return string       A single value for the named property
1612     */
1613    public function __get($name)
1614    {
1615        return $this->get($this->uri, $name);
1616    }
1617
1618    /** Magic method to set the value for a property of the graph
1619     *
1620     * Note that only properties in the default namespace can be accessed in this way.
1621     *
1622     * Example:
1623     *   $graph->title = 'Title';
1624     *
1625     * @see EasyRdf_Namespace::setDefault()
1626     * @param  string $name The name of the property
1627     * @param  string $value The value for the property
1628     */
1629    public function __set($name, $value)
1630    {
1631        return $this->set($this->uri, $name, $value);
1632    }
1633
1634    /** Magic method to check if a property exists
1635     *
1636     * Note that only properties in the default namespace can be accessed in this way.
1637     *
1638     * Example:
1639     *   if (isset($graph->title)) { blah(); }
1640     *
1641     * @see EasyRdf_Namespace::setDefault()
1642     * @param string $name The name of the property
1643     */
1644    public function __isset($name)
1645    {
1646        return $this->hasProperty($this->uri, $name);
1647    }
1648
1649    /** Magic method to delete a property of the graph
1650     *
1651     * Note that only properties in the default namespace can be accessed in this way.
1652     *
1653     * Example:
1654     *   unset($graph->title);
1655     *
1656     * @see EasyRdf_Namespace::setDefault()
1657     * @param string $name The name of the property
1658     */
1659    public function __unset($name)
1660    {
1661        return $this->delete($this->uri, $name);
1662    }
1663}
1664