From 74e8e2318d637a4de8fbbe871c874ed3a816c618 Mon Sep 17 00:00:00 2001 From: Marco Villegas Date: Tue, 19 Jan 2021 15:47:36 +0100 Subject: Import php-easyrdf_1.0.0.orig.tar.gz [dgit import orig php-easyrdf_1.0.0.orig.tar.gz] --- .travis.yml | 22 + CHANGELOG.md | 393 +++++++++ CODE_OF_CONDUCT.md | 130 +++ DEVELOPER.md | 31 + LICENSE.md | 420 ++++++++++ README.md | 126 +++ composer.json | 49 ++ doap.php | 44 ++ lib/Collection.php | 338 ++++++++ lib/Container.php | 234 ++++++ lib/Exception.php | 51 ++ lib/Format.php | 735 +++++++++++++++++ lib/Graph.php | 1753 +++++++++++++++++++++++++++++++++++++++++ lib/GraphStore.php | 312 ++++++++ lib/Http.php | 85 ++ lib/Http/Client.php | 575 ++++++++++++++ lib/Http/Exception.php | 18 + lib/Http/Response.php | 431 ++++++++++ lib/Isomorphic.php | 437 ++++++++++ lib/Literal.php | 342 ++++++++ lib/Literal/Boolean.php | 94 +++ lib/Literal/Date.php | 140 ++++ lib/Literal/DateTime.php | 119 +++ lib/Literal/Decimal.php | 127 +++ lib/Literal/HTML.php | 72 ++ lib/Literal/HexBinary.php | 93 +++ lib/Literal/Integer.php | 69 ++ lib/Literal/XML.php | 72 ++ lib/ParsedUri.php | 340 ++++++++ lib/Parser.php | 154 ++++ lib/Parser/Arc.php | 99 +++ lib/Parser/Exception.php | 77 ++ lib/Parser/Json.php | 158 ++++ lib/Parser/JsonLd.php | 127 +++ lib/Parser/Ntriples.php | 218 +++++ lib/Parser/Rapper.php | 107 +++ lib/Parser/RdfPhp.php | 134 ++++ lib/Parser/RdfXml.php | 815 +++++++++++++++++++ lib/Parser/Rdfa.php | 728 +++++++++++++++++ lib/Parser/Turtle.php | 1362 ++++++++++++++++++++++++++++++++ lib/RdfNamespace.php | 443 +++++++++++ lib/Resource.php | 828 +++++++++++++++++++ lib/Serialiser.php | 108 +++ lib/Serialiser/Arc.php | 105 +++ lib/Serialiser/GraphViz.php | 396 ++++++++++ lib/Serialiser/Json.php | 75 ++ lib/Serialiser/JsonLd.php | 148 ++++ lib/Serialiser/Ntriples.php | 228 ++++++ lib/Serialiser/Rapper.php | 108 +++ lib/Serialiser/RdfPhp.php | 77 ++ lib/Serialiser/RdfXml.php | 256 ++++++ lib/Serialiser/Turtle.php | 389 +++++++++ lib/Sparql/Client.php | 395 ++++++++++ lib/Sparql/Result.php | 390 +++++++++ lib/TypeMapper.php | 175 ++++ lib/Utils.php | 302 +++++++ scripts/copyright_updater.php | 64 ++ 57 files changed, 16118 insertions(+) create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 DEVELOPER.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 doap.php create mode 100644 lib/Collection.php create mode 100644 lib/Container.php create mode 100644 lib/Exception.php create mode 100644 lib/Format.php create mode 100644 lib/Graph.php create mode 100644 lib/GraphStore.php create mode 100644 lib/Http.php create mode 100644 lib/Http/Client.php create mode 100644 lib/Http/Exception.php create mode 100644 lib/Http/Response.php create mode 100644 lib/Isomorphic.php create mode 100644 lib/Literal.php create mode 100644 lib/Literal/Boolean.php create mode 100644 lib/Literal/Date.php create mode 100644 lib/Literal/DateTime.php create mode 100644 lib/Literal/Decimal.php create mode 100644 lib/Literal/HTML.php create mode 100644 lib/Literal/HexBinary.php create mode 100644 lib/Literal/Integer.php create mode 100644 lib/Literal/XML.php create mode 100644 lib/ParsedUri.php create mode 100644 lib/Parser.php create mode 100644 lib/Parser/Arc.php create mode 100644 lib/Parser/Exception.php create mode 100644 lib/Parser/Json.php create mode 100644 lib/Parser/JsonLd.php create mode 100644 lib/Parser/Ntriples.php create mode 100644 lib/Parser/Rapper.php create mode 100644 lib/Parser/RdfPhp.php create mode 100644 lib/Parser/RdfXml.php create mode 100644 lib/Parser/Rdfa.php create mode 100644 lib/Parser/Turtle.php create mode 100644 lib/RdfNamespace.php create mode 100644 lib/Resource.php create mode 100644 lib/Serialiser.php create mode 100644 lib/Serialiser/Arc.php create mode 100644 lib/Serialiser/GraphViz.php create mode 100644 lib/Serialiser/Json.php create mode 100644 lib/Serialiser/JsonLd.php create mode 100644 lib/Serialiser/Ntriples.php create mode 100644 lib/Serialiser/Rapper.php create mode 100644 lib/Serialiser/RdfPhp.php create mode 100644 lib/Serialiser/RdfXml.php create mode 100644 lib/Serialiser/Turtle.php create mode 100644 lib/Sparql/Client.php create mode 100644 lib/Sparql/Result.php create mode 100644 lib/TypeMapper.php create mode 100644 lib/Utils.php create mode 100644 scripts/copyright_updater.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5266c2a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: php +matrix: + include: + - php: 7.1 + dist: trusty + - php: 7.2 + dist: trusty + - php: 7.3 + dist: trusty + - php: 7.4 + dist: trusty + +sudo: required +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq graphviz + +install: make composer-install +script: + - make lint + - make cs + - make test-lib diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..25a9332 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,393 @@ +EasyRdf 1.0.0 +============= + +Major new features +------------------ + +* Minimum version of PHP is now PHP version 7.1, PHP 5.x is no-longer supported +* Usage without composer is not supported anymore +* Library is loaded via PSR-4 autoloader now +* The Redland PHP extension is no-longer supported + +Enhancements +------------ + +* `$graph->isA()` can take full IRIs as second parameter (only qname was accepted earlier, see issue #215) +* `Accept` HTTP-header depends on SPARQL-query type (see issues #231, #226) +* It is possible to set alternate default `Resource` class via `Graph::setDefaultResourceClass()` (see issue #243) +* When calling `Graph::load()` set the HTTP Accept header to the desired format +* The RDF/PHP and RDF/JSON specifications were added to the documentation +* Added text/xml and application/xml MIME types to RDF/XML format +* Added additional namespaces from W3C RDFa context +* Speeding up the turtle parser by optimising the mb_substr calls +* Added support for compressed response body in HTTP client +* Updated to PHP CodeSniffer v3 +* Updated to Sami v4 for API documentation +* Updated to PHPUnit v7 + +API changes +----------- + +* Classes are renamed like this: `EasyRdf_Parser_Turtle` → `EasyRdf\Parser\Turtle`. With a single exception: `EasyRdf_Namespace` → `EasyRdf\RdfNamespace` (because `namespace` is a keyword in PHP) +* EasyRdf expects HTTP-client objects compatible with ZendFramework 2.x instead of 1.x now. (zend-http is added to require-dev so tests for it are always run) +* `Resource` implements `ArrayAccess` interface now (see #242) +* Now using PHP Type Hints in some classes, which avoids having to do type checks +* Serialiser class is now abstract +* Implement the ArrayAccess Interface in Resource + +Bug Fixes +--------- + +* Fixes that add compatibility for PHP 7.4 +* Unicode-strings are properly encoded in n-triples documents (see #219) +* `RdfPhp` parser validates its input (see #227) +* Timeout is applied to response-times, not only connection-times (see #202) +* `$graph->get()` is reliable after `$graph->delete()` now (see #239, #241) +* Fix for running Graphviz tests against newer versions of Graphviz +* Fixed for when the HTTP server doesn't return Reason-Phrase in the status (see #321) +* Corrections to RDF format URIs +* Fixes for format guessing, so it works with SPARQL-style PREFIX and BASE +* Turtle serialiser improvements and fixes +* Fix for unescaping URIs while parsing ntriples +* Fixed encoding of unicode literals in ntriples + +Changes to Examples +------------------- + +* Added index.php to examples folder, to make them easier to navigate +* Removed artistinfo.php example (BBC Music no longer publishes RDF) +* Replaced Dbpedialite Villages example with Wikidata Villages example +* Added Open Graph Protocol example +* Changed default URI for converter example +* Fixed namespace for dbpedia categories +* Fixes for UK Postcode example and changed it to use Open Street Map + + +EasyRdf 0.9.1 +============= + +Bug Fixes +--------- +* Support timeouts for HTTP requests where the server takes a long time to answer. Fixes #202 +* Fixed Google Map on UK Postcode example + + +EasyRdf 0.9.0 +============= + +Major new features +------------------ +* Framing support in `EasyRdf_Serialiser_JsonLd` +* JSON-LD Parser + +API changes +----------- +* `EasyRdf_Literal_Decimal` returns strings, instead of floats to avoid losing precision (see issue #178) +* `EasyRdf_Literal_Decimal` requires input-strings which conform to `xs:decimal` format +* `EasyRdf_GraphStore` supports operations over default graph now +* `EasyRdf_Literal` typed as `xs:double` is used for PHP-floats instead of `EasyRdf_Literal_Decimal` +* Exceptions thrown from `EasyRdf_Graph::resource()` use different message-texts now (see issue #159) + +Enhancements +------------ +* Synced list of default namespaces against [RDFa Core Initial Context](http://www.w3.org/2011/rdfa-context/rdfa-1.1) rev.2014-01-17 +* Added support for empty prefixes (see issue #183) +* `EasyRdf_Graph::newAndLoad` throws `EasyRdf_Http_Exception` in case of failure, which gives access to status and response-body. (see issue #149) +* `EasyRdf_Graph` and `EasyRdf_Resource` have 'typesAsResources()' methods now + +Bug Fixes +--------- +* Fix for Turtle serialisation of FALSE (see issue #179) +* Fix for edge-case in RDF/XML serialisation (see issue #186) +* SPARQL-queries against endpoints which have query-params in their URL (see issue #184) +* Float values are properly handled if locale with "other" separator is active +* Fixed parsing of Turtle-documents with higher utf-8 characters (see issue #195) +* Namespace-prefixes are compliant with RDFXML QName spec (see issue #185) +* `EasyRdf_Namespace` won't generate "short" names with "/" in them anymore (see issue #115) +* `EasyRdf_Parser_RdfXml` respects "base" specified for the document (see issue #157) +* HTML documents are correctly detected now, not as "n-triples" (see issue #206) +* Accept-headers are formatted in locale-independent fashion now (see issue #208) + + +EasyRdf 0.8.0 +============= + +Major new features +------------------ +* Now PSR-2 compliant +* Added RDFa parser +* Added SPARQL Update support to `EasyRdf_Sparql_Client` + +API changes +----------- +* `is_a()` has been renamed to `isA()` +* `isBnode()` has been renamed to `isBNode()` +* `getNodeId()` has been renamed to `getBNodeId()` +* Added a `$value` property to `hasProperty()` +* Renamed `toArray()` to `toRdfPhp()` +* Renamed `count()` to `countValues()` in `EasyRdf_Graph` and `EasyRdf_Resource` +* Made passing a URI to `delete()` behave more like `all()` and `get()` - you must enclose in `<>` +* `dump(true)` has changed to `dump('html')` +* `getUri()` in `EasyRdf_Sparql_Client` has been renamed to `getQueryUri()` + +Enhancements +------------ +* Added `EasyRdf_Container` class to help iterate through `rdf:Alt`, `rdf:Bag` and `rdf:Seq` +* Added `EasyRdf_Collection` class to help iterate through `rdf:List` +* Added `EasyRdf_Literal_HTML` and `EasyRdf_Literal_XML` +* Changed formatting of `xsd:dateTime` from `DateTime::ISO8601` to `DateTime::ATOM` +* Added `rss:title` to the list of properties that `label()` will check for +* Added support for serialising containers to the RDF/XML serialiser +* Added getGraph method to `EasyRdf_Resource` +* Turtle parser improvements +* Added the `application/n-triples` MIME type for the N-Triples format +* Added support to `EasyRdf_Namespace` for expanding `a` to `rdf:type` +* Added `listNamedGraphs()` function to `EasyRdf_Sparql_Client` +* Added line and column number to exceptions in the built-in parsers + +Bug Fixes +--------- +* Fixed bug in `EasyRdf_Namespace::expand()` (see issue #114) +* Fix for dumping SPARQL SELECT query with unbound result (see issue #112) +* Sesame compatibility : avoid duplicate Content-Length header +* Fix for for passing objects of type DateTime to $graph->add() (see issue #119) +* Fix for SPARQL queries longer than 2KB (see issue #85) +* Fix for dumping literal with unshortenable datatype uri (see issue #120) +* Fix for getting default mime type or extension when there isn't one +* Fix for missing trailing slash the HTTP client + + +EasyRdf 0.7.2 +============= + +Enhancements +------------ +* Removed automatic registration of ARC2 and librdf parsers and serialisers +** You must now specifically choose the parser or serialiser +* Refactored `EasyRdf_Literal` with datatypes so that it preserves exact value +* Changed Turtle serialiser to not escape Unicode characters unnecessarily +* Fix for escaping literals objects in Turtle serialiser +* Added a new static function `newAndLoad()` to `EasyRdf_Graph` +* Added setters for each of the components of the URI to the class `EasyRdf_ParsedUri` +* Added option to the converter example, to allow raw output, without any HTML + +Bug Fixes +--------- +* Fixed broken Redland parser (thanks to Jon Phipps) +* Fix for serialising two bnodes that reference each other in Turtle +* Added support for parsing literals with single quotes in Turtle +* Removed require for EasyRdf/Exception.php +* Fix for serialising `EasyRdf_Literal_DateTime` to Turtle +* Fix for serialising Turtle literals with a shorthand syntax +* Several typo fixes and minor corrections + + +EasyRdf 0.7.1 +============= + +Enhancements +------------ +* Changed minimum version of PHPUnit to 3.5.15 +* Added RDFa namespace +* Added Open Graph Protocol namespace +* Made improvements to formatting of the Turtle serialiser +* Added new splitUri() function to EasyRdf_Namespace +* Made improvements to format guessing + +Bug Fixes +--------- +* Fix for RDF/XML parser not returning the number of triples +* Added re-mapping of b-nodes to N-Triples and Redland parsers + + +EasyRdf 0.7.0 +============= + +API Changes +----------- +* You must now wrap full property URIs in angle brackets + +Major new features +------------------ +* Added a new pure-PHP Turtle parser +* Added basic property-path support for traversing graphs +* Added support for serialising to the GraphViz dot format (and generating images) +* Added a new class `EasyRdf_ParsedUri` - a RFC3986 compliant URI parser + +Enhancements +------------ +* The load() function in `EasyRdf_Graph` no-longer takes a $data argument +* The parse() and load() methods, now return the number of triples parsed +* Added count() method to `EasyRdf_Resource` and `EasyRdf_Graph` +* Added localName() method to `EasyRdf_Resource` +* Added htmlLink() method to `EasyRdf_Resource` +* Added methods deleteResource() and deleteLiteral() to `EasyRdf_Graph` +* Added support for guessing the file format based on the file extension +* Performance improvements to built-in serialisers + +Environment changes +------------------- +* Added PHP Composer description to the project +* Now properly PSR-0 autoloader compatible +* New minimum version of PHP is 5.2.8 +* Changed test suite to require PHPUnit 3.6 +* Changed from Phing to GNU Make based build system +* Added automated testing of the examples + +Bug Fixes +--------- +* Fix for loading https:// URLs +* Fix for storing the value 0 in a `EasyRdf_Graph` +* Fix for HTTP servers that return relative URIs in the Location header +* Fix for Literals with languages in the SPARQL Query Results XML Format +* Fix for SPARQL servers that put extra whitespace into the XML result +* Fix for the httpget.php example in PHP 5.4+ + + +EasyRdf 0.6.3 +============= +* Added $graph->parseFile() method. +* Added support for SSL (https) to the built-in HTTP client +* Fixes for HTTP responses with a charset parameter in the Content Type. +* Improved error handling and empty documents in JSON and rapper parsers. +* Added connivence class for xsd:hexBinary literals: + - `EasyRdf_Literal_HexBinary` +* Made EasyRdf more tolerant of 'badly serialised bnodes' +* Fix for SPARQL servers that return charset in the MIME Type. +* Fix for using xml:lang in SPARQL 1.1 Query Results JSON Format +* Changed datetime ISO formatting to use 'Z' instead of +0000 for UTC dateTimes +* Added the namespace for 'The Cert Ontology' to EasyRdf. + + +EasyRdf 0.6.2 +============= +* Bug fix for missing triples in the RDF/XML serialiser. +* Added countTriples() method to `EasyRdf_Graph`. +* Re-factored the mechanism for mapping RDF datatypes to PHP classes. +* Added subclasses of `EasyRdf_Literal` for various XSD datatypes: + - `EasyRdf_Literal_Boolean` + - `EasyRdf_Literal_Date` + - `EasyRdf_Literal_DateTime` + - `EasyRdf_Literal_Decimal` + - `EasyRdf_Literal_Integer` +* Made the Redland based parser write triples directly to `EasyRdf_Graph` +* Added support for datatypes and languages in the `EasyRdf_Parser_Ntriples` parser. +* Fix for parsing XML Literals in RDF/XML + + +EasyRdf 0.6.1 +============= +* Updated API documentation for new classes and methods added in 0.6.0 +* Added a description to the top of the source code for each example. +* Changed the generated bnode identifier names from eidXXX to genidXXX. +* Implemented inlining of resources in the RDF/XML serialiser. +* Added new reversePropertyUris() method to `EasyRdf_Graph` and `EasyRdf_Resource`. +* Added addType() and setType() to `EasyRdf_Resource`. +* Added a textarea to the converter example. +* Added support for parsing the json-triples format. +* Renamed `EasyRdf_SparqlClient` to `EasyRdf_Sparql_Client` +* Renamed `EasyRdf_SparqlResult` to `EasyRdf_Sparql_Result` +* Fix for $graph->isEmpty() failing after adding and deleting some triples +* Added new `EasyRdf_DatatypeMapper` class that allows you to map RDF datatypes to PHP classes. +* Renamed guessDatatype() to getDatatypeForValue() in `EasyRdf_Literal`. +* Added getResource() and allResources() to `EasyRdf_Graph` and `EasyRdf_Resource` +* Implemented value casting in literals based on the datatype. + + +EasyRdf 0.6.0 +============= +* Major re-factor of the way data is stored internally in `EasyRdf_Graph`. +* Parsing and serialising is now much faster and will enable further optimisations. +* API is mostly backwards-compatible apart from: + - Changed inverse property operator from - to ^ to match Sparql 1.1 property paths. + - New `EasyRdf_Graphs` will not automatically be loaded on creation + You must now call $graph->load(); + - Setting the default HTTP client is now part of a new `EasyRdf_Http` class + - It is no-longer possible to add multiple properties at once using an associative array. +* Added methods to `EasyRdf_Graph` for direct manipulation of triples. +* Added new `EasyRdf_GraphStore` - class for fetching, saving and deleting graphs to a Graph Store over HTTP. +* Added new `EasyRdf_SparqlClient` and `EasyRdf_SparqlResult` - class for querying a SPARQL endpoint over HTTP. +* Added q values for each Mime-Type associated with an `EasyRdf_Format`. +* New example demonstrating integration with the Zend Framework. +* New `EasyRdf_HTTP_MockClient` class makes testing easier. + + +EasyRdf 0.5.2 +============= +* Added a built-in RDF/XML parser +* Made the RDF/XML serialiser use the rdf:type to open tags +* Added support for comments in the N-Triples parser +* Added new resolveUriReference() function to `EasyRdf_Utils` +* Added the application/rdf+json and text/rdf+n3 mime types + + +EasyRdf 0.5.1 +============= +* Bug fixes for PHP 5.2 + + +EasyRdf 0.5.0 +============= +* Added support for inverse properties. +* Updated RDF/XML and Turtle serialisers to create new namespaces if possible. +* Added new is_a($type) method to `EasyRdf_Resource`. +* Added support for passing an array of properties to the get() method. +* Added primaryTopic() method to `EasyRdf_Resource`. +* The function label() in `EasyRdf_Resource` will no longer attempted to shorten the URI, + if there is no label available. +* Resource types are now stored as resources, instead of shortened URIs. +* Added support for deleting a specific value for property to `EasyRdf_Resource`. +* Properties and datatypes are now stored as full URIs and not + converted to qnames during import. +* Change the TypeMapper to store full URIs internally. +* Added bibo and geo to the set of default namespaces. +* Improved bnode links in dump format +* Fix for converting non-string `EasyRdf_Literal` to string. +* Created an example that resolves UK postcodes using uk-postcodes.com. + + +EasyRdf 0.4.0 +============= +* Moved source code to Github +* Added an `EasyRdf_Literal` class +* Added proper support for Datatypes and Languages +* Added built-in RDF/XML serialiser +* Added built-in Turtle serialiser +* Added a new `EasyRdf_Format` class to deal with mime types etc. +* finished a major refactoring of the Parser/Serialiser registration +* removed all parsing related code from `EasyRdf_Graph` +* Added a basic serialisation example +* Added additional common namespaces +* Test fixes + + +EasyRdf 0.3.0 +============= +* Generated Wiki pages from phpdoc +* Filtering of literals by language +* Moved parsers into `EasyRdf_Parser_XXX` namespace +* Added support for serialisation +* Wrote RDF generation example (foafmaker.php) +* Added built-in ntriples parser/generator +* Added built-in RDF/PHP serialiser +* Added built-in RDF/JSON serialiser +* Added SKOS and RSS to the set of default namespaces. + + +EasyRdf 0.2.0 +============= +* Added support for Redland PHP bindings +* Added support for n-triples document type. +* Improved blank node handing and added newBNode() method to `EasyRdf_Graph`. +* Add option to `EasyRdf_RapperParser` to choose location of rapper command +* Added Rails style HTML tag helpers to examples to make them simpler + + +EasyRdf 0.1.0 +============= +* First public release +* Support for ARC2 and Rapper +* Built-in HTTP Client +* API Documentation +* PHP Unit tests for every class. +* Several usage examples diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..824fe1d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,130 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +njh@aelius.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. + diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..f9c6dac --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,31 @@ +Contributing to EasyRdf +======================= + +Contributions to the EasyRdf codebase are welcome using the usual Github pull request workflow. + +To run the code style checker: + +``` +make cs +``` + +You can run the PHP unit test suite with: + +``` +make test-lib +``` + +Unit tests are automatically run after being received by Github: +http://ci.aelius.com/job/easyrdf/ + +The tests for the examples are run separately: +http://ci.aelius.com/job/easyrdf-examples/ + + +Notes +----- + +* Please ask on the [mailing list] before starting work on any significant changes +* Please write tests for any new features or bug fixes. The tests should be checked in the same commit as the code. + +[mailing list]:http://groups.google.com/group/easyrdf diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4a79f79 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,420 @@ +LICENSE +======= + +Copyright (c) 2009-2020 Nicholas J Humfrey. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * The name of the author 'Nicholas J Humfrey" may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +OTHER LICENSES +============== + +Parts of this program or documentation is available under different +licensing terms. These are as following. + +The appendix in the documentation about RDF formats (APPENDIX A) is a +derivative work under CC-BY-SA-3.0. It consists of two documents: + +1. The RDF/PHP Specification was written/edited 2008 by Ian Davis and Keith + Alexander +2. The RDF/JSON Specification was written/edited 2007, 2008 by Keith + Alexander, Danny Ayers, Sam Tunnicliffe, Fellahst, Ian Davis and Robman + +These two documents have been translated 2014 into markdown by hakre. + + +Creative Commons Attribution-ShareAlike 3.0 Unported +==================================================== + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE +LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN +ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION +ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE +INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM +ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + +a. "Adaptation" means a work based upon the Work, or upon the Work and +other pre-existing works, such as a translation, adaptation, derivative +work, arrangement of music or other alterations of a literary or +artistic work, or phonogram or performance and includes cinematographic +adaptations or any other form in which the Work may be recast, +transformed, or adapted including in any form recognizably derived from +the original, except that a work that constitutes a Collection will not +be considered an Adaptation for the purpose of this License. For the +avoidance of doubt, where the Work is a musical work, performance or +phonogram, the synchronization of the Work in timed-relation with a +moving image ("synching") will be considered an Adaptation for the +purpose of this License. + +b. "Collection" means a collection of literary or artistic works, such +as encyclopedias and anthologies, or performances, phonograms or +broadcasts, or other works or subject matter other than works listed in +Section 1(f) below, which, by reason of the selection and arrangement of +their contents, constitute intellectual creations, in which the Work is +included in its entirety in unmodified form along with one or more other +contributions, each constituting separate and independent works in +themselves, which together are assembled into a collective whole. A work +that constitutes a Collection will not be considered an Adaptation (as +defined below) for the purposes of this License. + +c. "Creative Commons Compatible License" means a license that is listed +at http://creativecommons.org/compatiblelicenses that has been approved +by Creative Commons as being essentially equivalent to this License, +including, at a minimum, because that license: (i) contains terms that +have the same purpose, meaning and effect as the License Elements of +this License; and, (ii) explicitly permits the relicensing of +adaptations of works made available under that license under this +License or a Creative Commons jurisdiction license with the same License +Elements as this License. + +d. "Distribute" means to make available to the public the original and +copies of the Work or Adaptation, as appropriate, through sale or other +transfer of ownership. + +e. "License Elements" means the following high-level license attributes +as selected by Licensor and indicated in the title of this License: +Attribution, ShareAlike. + +f. "Licensor" means the individual, individuals, entity or entities that +offer(s) the Work under the terms of this License. + +g. "Original Author" means, in the case of a literary or artistic work, +the individual, individuals, entity or entities who created the Work or +if no individual or entity can be identified, the publisher; and in +addition (i) in the case of a performance the actors, singers, +musicians, dancers, and other persons who act, sing, deliver, declaim, +play in, interpret or otherwise perform literary or artistic works or +expressions of folklore; (ii) in the case of a phonogram the producer +being the person or legal entity who first fixes the sounds of a +performance or other sounds; and, (iii) in the case of broadcasts, the +organization that transmits the broadcast. + +h. "Work" means the literary and/or artistic work offered under the +terms of this License including without limitation any production in the +literary, scientific and artistic domain, whatever may be the mode or +form of its expression including digital form, such as a book, pamphlet +and other writing; a lecture, address, sermon or other work of the same +nature; a dramatic or dramatico-musical work; a choreographic work or +entertainment in dumb show; a musical composition with or without words; +a cinematographic work to which are assimilated works expressed by a +process analogous to cinematography; a work of drawing, painting, +architecture, sculpture, engraving or lithography; a photographic work +to which are assimilated works expressed by a process analogous to +photography; a work of applied art; an illustration, map, plan, sketch +or three-dimensional work relative to geography, topography, +architecture or science; a performance; a broadcast; a phonogram; a +compilation of data to the extent it is protected as a copyrightable +work; or a work performed by a variety or circus performer to the extent +it is not otherwise considered a literary or artistic work. + +i. "You" means an individual or entity exercising rights under this +License who has not previously violated the terms of this License with +respect to the Work, or who has received express permission from the +Licensor to exercise rights under this License despite a previous +violation. + +j. "Publicly Perform" means to perform public recitations of the Work +and to communicate to the public those public recitations, by any means +or process, including by wire or wireless means or public digital +performances; to make available to the public Works in such a way that +members of the public may access these Works from a place and at a place +individually chosen by them; to perform the Work to the public by any +means or process and the communication to the public of the performances +of the Work, including by public digital performance; to broadcast and +rebroadcast the Work by any means including signs, sounds or images. + +k. "Reproduce" means to make copies of the Work by any means including +without limitation by sound or visual recordings and the right of +fixation and reproducing fixations of the Work, including storage of a +protected performance or phonogram in digital form or other electronic +medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + +a. to Reproduce the Work, to incorporate the Work into one or more +Collections, and to Reproduce the Work as incorporated in the +Collections; + +b. to create and Reproduce Adaptations provided that any such +Adaptation, including any translation in any medium, takes reasonable +steps to clearly label, demarcate or otherwise identify that changes +were made to the original Work. For example, a translation could be +marked "The original work was translated from English to Spanish," or a +modification could indicate "The original work has been modified."; + +c. to Distribute and Publicly Perform the Work including as incorporated +in Collections; and, + +d. to Distribute and Publicly Perform Adaptations. + +e. For the avoidance of doubt: + +i. Non-waivable Compulsory License Schemes. In those jurisdictions in +which the right to collect royalties through any statutory or compulsory +licensing scheme cannot be waived, the Licensor reserves the exclusive +right to collect such royalties for any exercise by You of the rights +granted under this License; + +ii. Waivable Compulsory License Schemes. In those jurisdictions in which +the right to collect royalties through any statutory or compulsory +licensing scheme can be waived, the Licensor waives the exclusive right +to collect such royalties for any exercise by You of the rights granted +under this License; and, + +iii. Voluntary License Schemes. The Licensor waives the right to collect +royalties, whether individually or, in the event that the Licensor is a +member of a collecting society that administers voluntary licensing +schemes, via that society, from any exercise by You of the rights +granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights +in other media and formats. Subject to Section 8(f), all rights not +expressly granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly +made subject to and limited by the following restrictions: + +a. You may Distribute or Publicly Perform the Work only under the terms +of this License. You must include a copy of, or the Uniform Resource +Identifier (URI) for, this License with every copy of the Work You +Distribute or Publicly Perform. You may not offer or impose any terms on +the Work that restrict the terms of this License or the ability of the +recipient of the Work to exercise the rights granted to that recipient +under the terms of the License. You may not sublicense the Work. You +must keep intact all notices that refer to this License and to the +disclaimer of warranties with every copy of the Work You Distribute or +Publicly Perform. When You Distribute or Publicly Perform the Work, You +may not impose any effective technological measures on the Work that +restrict the ability of a recipient of the Work from You to exercise the +rights granted to that recipient under the terms of the License. This +Section 4(a) applies to the Work as incorporated in a Collection, but +this does not require the Collection apart from the Work itself to be +made subject to the terms of this License. If You create a Collection, +upon notice from any Licensor You must, to the extent practicable, +remove from the Collection any credit as required by Section 4(c), as +requested. If You create an Adaptation, upon notice from any Licensor +You must, to the extent practicable, remove from the Adaptation any +credit as required by Section 4(c), as requested. + +b. You may Distribute or Publicly Perform an Adaptation only under the +terms of: (i) this License; (ii) a later version of this License with +the same License Elements as this License; (iii) a Creative Commons +jurisdiction license (either this or a later license version) that +contains the same License Elements as this License (e.g., +Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible +License. If you license the Adaptation under one of the licenses +mentioned in (iv), you must comply with the terms of that license. If +you license the Adaptation under the terms of any of the licenses +mentioned in (i), (ii) or (iii) (the "Applicable License"), you must +comply with the terms of the Applicable License generally and the +following provisions: (I) You must include a copy of, or the URI for, +the Applicable License with every copy of each Adaptation You Distribute +or Publicly Perform; (II) You may not offer or impose any terms on the +Adaptation that restrict the terms of the Applicable License or the +ability of the recipient of the Adaptation to exercise the rights +granted to that recipient under the terms of the Applicable License; +(III) You must keep intact all notices that refer to the Applicable +License and to the disclaimer of warranties with every copy of the Work +as included in the Adaptation You Distribute or Publicly Perform; (IV) +when You Distribute or Publicly Perform the Adaptation, You may not +impose any effective technological measures on the Adaptation that +restrict the ability of a recipient of the Adaptation from You to +exercise the rights granted to that recipient under the terms of the +Applicable License. This Section 4(b) applies to the Adaptation as +incorporated in a Collection, but this does not require the Collection +apart from the Adaptation itself to be made subject to the terms of the +Applicable License. + +c. If You Distribute, or Publicly Perform the Work or any Adaptations or +Collections, You must, unless a request has been made pursuant to +Section 4(a), keep intact all copyright notices for the Work and +provide, reasonable to the medium or means You are utilizing: (i) the +name of the Original Author (or pseudonym, if applicable) if supplied, +and/or if the Original Author and/or Licensor designate another party or +parties (e.g., a sponsor institute, publishing entity, journal) for +attribution ("Attribution Parties") in Licensor's copyright notice, +terms of service or by other reasonable means, the name of such party or +parties; (ii) the title of the Work if supplied; (iii) to the extent +reasonably practicable, the URI, if any, that Licensor specifies to be +associated with the Work, unless such URI does not refer to the +copyright notice or licensing information for the Work; and (iv) , +consistent with Ssection 3(b), in the case of an Adaptation, a credit +identifying the use of the Work in the Adaptation (e.g., "French +translation of the Work by Original Author," or "Screenplay based on +original Work by Original Author"). The credit required by this Section +4(c) may be implemented in any reasonable manner; provided, however, +that in the case of a Adaptation or Collection, at a minimum such credit +will appear, if a credit for all contributing authors of the Adaptation +or Collection appears, then as part of these credits and in a manner at +least as prominent as the credits for the other contributing authors. +For the avoidance of doubt, You may only use the credit required by this +Section for the purpose of attribution in the manner set out above and, +by exercising Your rights under this License, You may not implicitly or +explicitly assert or imply any connection with, sponsorship or +endorsement by the Original Author, Licensor and/or Attribution Parties, +as appropriate, of You or Your use of the Work, without the separate, +express prior written permission of the Original Author, Licensor and/or +Attribution Parties. + +d. Except as otherwise agreed in writing by the Licensor or as may be +otherwise permitted by applicable law, if You Reproduce, Distribute or +Publicly Perform the Work either by itself or as part of any Adaptations +or Collections, You must not distort, mutilate, modify or take other +derogatory action in relation to the Work which would be prejudicial to +the Original Author's honor or reputation. Licensor agrees that in those +jurisdictions (e.g. Japan), in which any exercise of the right granted +in Section 3(b) of this License (the right to make Adaptations) would be +deemed to be a distortion, mutilation, modification or other derogatory +action prejudicial to the Original Author's honor and reputation, the +Licensor will waive or not assert, as appropriate, this Section, to the +fullest extent permitted by the applicable national law, to enable You +to reasonably exercise Your right under Section 3(b) of this License +(right to make Adaptations) but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE +EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + +a. This License and the rights granted hereunder will terminate +automatically upon any breach by You of the terms of this License. +Individuals or entities who have received Adaptations or Collections +from You under this License, however, will not have their licenses +terminated provided such individuals or entities remain in full +compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will +survive any termination of this License. + +b. Subject to the above terms and conditions, the license granted here +is perpetual (for the duration of the applicable copyright in the Work). +Notwithstanding the above, Licensor reserves the right to release the +Work under different license terms or to stop distributing the Work at +any time; provided, however that any such election will not serve to +withdraw this License (or any other license that has been, or is +required to be, granted under the terms of this License), and this +License will continue in full force and effect unless terminated as +stated above. + +8. Miscellaneous + +a. Each time You Distribute or Publicly Perform the Work or a +Collection, the Licensor offers to the recipient a license to the Work +on the same terms and conditions as the license granted to You under +this License. + +b. Each time You Distribute or Publicly Perform an Adaptation, Licensor +offers to the recipient a license to the original Work on the same terms +and conditions as the license granted to You under this License. + +c. If any provision of this License is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this License, and without further action +by the parties to this agreement, such provision shall be reformed to +the minimum extent necessary to make such provision valid and +enforceable. + +d. No term or provision of this License shall be deemed waived and no +breach consented to unless such waiver or consent shall be in writing +and signed by the party to be charged with such waiver or consent. + +e. This License constitutes the entire agreement between the parties +with respect to the Work licensed here. There are no understandings, +agreements or representations with respect to the Work not specified +here. Licensor shall not be bound by any additional provisions that may +appear in any communication from You. This License may not be modified +without the mutual written agreement of the Licensor and You. + +f. The rights granted under, and the subject matter referenced, in this +License were drafted utilizing the terminology of the Berne Convention +for the Protection of Literary and Artistic Works (as amended on +September 28, 1979), the Rome Convention of 1961, the WIPO Copyright +Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and +the Universal Copyright Convention (as revised on July 24, 1971). These +rights and subject matter take effect in the relevant jurisdiction in +which the License terms are sought to be enforced according to the +corresponding provisions of the implementation of those treaty +provisions in the applicable national law. If the standard suite of +rights granted under applicable copyright law includes additional rights +not granted under this License, such additional rights are deemed to be +included in the License; this License is not intended to restrict the +license of any rights under applicable law. + + + +Creative Commons Notice + +Creative Commons is not a party to this License, and makes no warranty +whatsoever in connection with the Work. Creative Commons will not be +liable to You or any party on any legal theory for any damages +whatsoever, including without limitation any general, special, +incidental or consequential damages arising in connection to this +license. Notwithstanding the foregoing two (2) sentences, if Creative +Commons has expressly identified itself as the Licensor hereunder, it +shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work +is licensed under the CCPL, Creative Commons does not authorize the use +by either party of the trademark "Creative Commons" or any related +trademark or logo of Creative Commons without the prior written consent +of Creative Commons. Any permitted use will be in compliance with +Creative Commons' then-current trademark usage guidelines, as may be +published on its website or otherwise made available upon request from +time to time. For the avoidance of doubt, this trademark restriction +does not form part of the License. + +Creative Commons may be contacted at http://creativecommons.org/. diff --git a/README.md b/README.md new file mode 100644 index 0000000..25d3dd4 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +EasyRdf +======= + +[![Build Status](https://travis-ci.com/easyrdf/easyrdf.svg?branch=master)](https://travis-ci.com/easyrdf/easyrdf) + +EasyRdf is a PHP library designed to make it easy to consume and produce [RDF]. +It was designed for use in mixed teams of experienced and inexperienced RDF +developers. It is written in Object Oriented PHP and has been tested +extensively using PHPUnit. + +After parsing EasyRdf builds up a graph of PHP objects that can then be walked +around to get the data to be placed on the page. Dump methods are available to +inspect what data is available during development. + +Data is typically loaded into an [`EasyRdf\Graph`] object from source RDF +documents, loaded from the web via HTTP. The [`EasyRdf\GraphStore`] class +simplifies loading and saving data to a SPARQL 1.1 Graph Store. + +SPARQL queries can be made over HTTP to a Triplestore using the +[`EasyRdf\Sparql\Client`] class. `SELECT` and `ASK` queries will return an +[`EasyRdf\Sparql\Result`] object and `CONSTRUCT` and `DESCRIBE` queries will return +an [`EasyRdf\Graph`] object. + +### Example ### + +```php +$foaf = new \EasyRdf\Graph("http://njh.me/foaf.rdf"); +$foaf->load(); +$me = $foaf->primaryTopic(); +echo "My name is: ".$me->get('foaf:name')."\n"; +``` + +Downloads +--------- + +The latest _stable_ version of EasyRdf can be [downloaded from the EasyRdf website]. + + +Links +----- + +* [EasyRdf Homepage](https://www.easyrdf.org/) +* [API documentation](https://www.easyrdf.org/docs/api) +* [Change Log](https://github.com/easyrdf/easyrdf/blob/master/CHANGELOG.md) +* [Source Code](https://github.com/easyrdf/easyrdf) +* [Issue Tracker](https://github.com/easyrdf/easyrdf/issues) + + +Requirements +------------ + +* PHP 7.1 or higher + + +Features +-------- + +* API documentation written in `phpdoc` +* Extensive unit tests written using `phpunit` +* Built-in parsers and serialisers: RDF/JSON, N-Triples, RDF/XML, Turtle +* Optional parsing support for: [ARC2], [rapper] +* Optional support for [`Zend\Http\Client`] +* No required external dependancies upon other libraries (PEAR, Zend, etc...) +* Complies with Zend Framework coding style. +* Type mapper - resources of type `foaf:Person` can be mapped into PHP object of class `Foaf_Person` +* Support for visualisation of graphs using [GraphViz] +* Comes with a number of examples + + +List of Examples +---------------- + +* [`basic.php`](/examples/basic.php#slider) - Basic "Hello World" type example +* [`basic_sparql.php`](/examples/basic_sparql.php#slider) - Example of making a SPARQL `SELECT` query +* [`converter.php`](/examples/converter.php#slider) - Convert RDF from one format to another +* [`dump.php`](/examples/dump.php#slider) - Display the contents of a graph +* [`foafinfo.php`](/examples/foafinfo.php#slider) - Display the basic information in a FOAF document +* [`foafmaker.php`](/examples/foafmaker.php#slider) - Construct a FOAF document with a choice of serialisations +* [`graph_direct.php`](/examples/graph_direct.php#slider) - Example of using `EasyRdf\Graph` directly without `EasyRdf\Resource` +* [`graphstore.php`](/examples/graphstore.php#slider) - Store and retrieve data from a SPARQL 1.1 Graph Store +* [`graphviz.php`](/examples/graphviz.php#slider) - GraphViz rendering example +* [`html_tag_helpers.php`](/examples/html_tag_helpers.php#slider) - Rails Style html tag helpers to make the EasyRdf examples simpler +* [`httpget.php`](/examples/httpget.php#slider) - No RDF, just test `EasyRdf\Http\Client` +* [`open_graph_protocol.php`](/examples/open_graph_protocol.php#slider) - Extract Open Graph Protocol metadata from a webpage +* [`serialise.php`](/examples/serialise.php#slider) - Basic serialisation example +* [`sparql_queryform.php`](/examples/sparql_queryform.php#slider) - Form to submit SPARQL queries and display the result +* [`uk_postcode.php`](/examples/uk_postcode.php#slider) - Example of resolving UK postcodes using uk-postcodes.com +* [`wikidata_villages.php`](/examples/wikidata_villages.php#slider) - Fetch and information about villages in Fife from Wikidata +* [`zend_framework.php`](/examples/zend_framework.php#slider) - Example of using `Zend\Http\Client` with EasyRdf + + +Running Examples +---------------- + +The easiest way of trying out some of the examples is to use the PHP command to +run a local web server on your computer. + +``` +php -S localhost:8080 -t examples +``` + +Then open the following URL in your browser: http://localhost:8080/ + + +Licensing +--------- + +The EasyRdf library and tests are licensed under the [BSD-3-Clause] license. +The examples are in the public domain, for more information see [UNLICENSE]. + + + +[`EasyRdf\Graph`]:https://www.easyrdf.org/docs/api/EasyRdf\Graph.html +[`EasyRdf\GraphStore`]:https://www.easyrdf.org/docs/api/EasyRdf\GraphStore.html +[`EasyRdf\Sparql\Client`]:https://www.easyrdf.org/docs/api/EasyRdf\Sparql\Client.html +[`EasyRdf\Sparql\Result`]:https://www.easyrdf.org/docs/api/EasyRdf\Sparql\Result.html + +[ARC2]:https://github.com/semsol/arc2/ +[BSD-3-Clause]:https://www.opensource.org/licenses/BSD-3-Clause +[downloaded from the EasyRdf website]:https://www.easyrdf.org/downloads +[GraphViz]:https://www.graphviz.org/ +[rapper]:http://librdf.org/raptor/rapper.html +[RDF]:https://en.wikipedia.org/wiki/Resource_Description_Framework +[SPARQL 1.1 query language]:https://www.w3.org/TR/sparql11-query/ +[UNLICENSE]:https://unlicense.org/ +[`Zend\Http\Client`]:https://docs.zendframework.com/zend-http/client/intro/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8b3b09c --- /dev/null +++ b/composer.json @@ -0,0 +1,49 @@ +{ + "name": "easyrdf/easyrdf", + "version": "1.0.0", + "description": "EasyRdf is a PHP library designed to make it easy to consume and produce RDF.", + "type": "library", + "keywords": ["RDF", "Semantic Web", "Linked Data", "Turtle", "RDFa", "SPARQL"], + "homepage": "http://www.easyrdf.org/", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Nicholas Humfrey", + "email": "njh@aelius.com", + "homepage": "http://www.aelius.com/njh/", + "role": "Developer" + }, + { + "name": "Alexey Zakhlestin", + "email": "indeyets@gmail.com", + "homepage": "http://indeyets.ru/", + "role": "Developer" + } + ], + "support": { + "forum": "http://groups.google.com/group/easyrdf/", + "issues": "http://github.com/easyrdf/easyrdf/issues" + }, + "require": { + "php": ">=7.1.0", + "ext-mbstring": "*", + "ext-pcre": "*" + }, + "suggest": { + "ml/json-ld": "~1.0", + "semsol/arc2": "~2.2" + }, + "require-dev": { + "ml/json-ld": "~1.0", + "phpunit/phpunit": "^7", + "sami/sami": "^4", + "semsol/arc2": "~2.2", + "squizlabs/php_codesniffer": "3.*", + "zendframework/zend-http": "~2.3" + }, + "autoload": { + "psr-4": { + "EasyRdf\\": "lib" + } + } +} diff --git a/doap.php b/doap.php new file mode 100644 index 0000000..e27f4ae --- /dev/null +++ b/doap.php @@ -0,0 +1,44 @@ +homepage.'doap.rdf'); + $easyrdf = $doap->resource('#easyrdf', 'doap:Project', 'foaf:Project'); + $easyrdf->addLiteral('doap:name', 'EasyRDF'); + $easyrdf->addLiteral('doap:shortname', 'easyrdf'); + $easyrdf->addLiteral('doap:revision', $composer->version); + $easyrdf->addLiteral('doap:shortdesc', $composer->description, 'en'); + $easyrdf->addResource('doap:homepage', $composer->homepage); + + $easyrdf->addLiteral('doap:programming-language', 'PHP'); + $easyrdf->addLiteral( + 'doap:description', 'EasyRdf is a PHP library designed to make it easy to consume and produce RDF. '. + 'It was designed for use in mixed teams of experienced and inexperienced RDF developers. '. + 'It is written in Object Oriented PHP and has been tested extensively using PHPUnit.', 'en' + ); + $easyrdf->addResource('doap:license', 'http://usefulinc.com/doap/licenses/bsd'); + $easyrdf->addResource('doap:download-page', 'http://github.com/easyrdf/easyrdf/downloads'); + $easyrdf->addResource('doap:bug-database', 'http://github.com/easyrdf/easyrdf/issues'); + $easyrdf->addResource('doap:mailing-list', 'http://groups.google.com/group/easyrdf'); + + $easyrdf->addResource('doap:category', 'http://dbpedia.org/resource/Resource_Description_Framework'); + $easyrdf->addResource('doap:category', 'http://dbpedia.org/resource/PHP'); + $easyrdf->addResource('doap:category', 'http://www.dbpedialite.org/things/24131#id'); + $easyrdf->addResource('doap:category', 'http://www.dbpedialite.org/things/53847#id'); + + $repository = $doap->newBNode('doap:GitRepository'); + $repository->addResource('doap:browse', 'http://github.com/easyrdf/easyrdf'); + $repository->addResource('doap:location', 'git://github.com/easyrdf/easyrdf.git'); + $easyrdf->addResource('doap:repository', $repository); + + $njh = $doap->resource('http://njh.me/', 'foaf:Person'); + $njh->addLiteral('foaf:name', 'Nicholas J Humfrey'); + $njh->addResource('foaf:homepage', 'http://www.aelius.com/njh/'); + $easyrdf->add('doap:maintainer', $njh); + $easyrdf->add('doap:developer', $njh); + $easyrdf->add('foaf:maker', $njh); + + print $doap->serialise('rdfxml'); diff --git a/lib/Collection.php b/lib/Collection.php new file mode 100644 index 0000000..7e31014 --- /dev/null +++ b/lib/Collection.php @@ -0,0 +1,338 @@ +position = 1; + $this->current = null; + parent::__construct($uri, $graph); + } + + /** Seek to a specific position in the container + * + * The first item is postion 1 + * + * @param integer $position The position in the container to seek to + * + * @throws \OutOfBoundsException + * @throws \InvalidArgumentException + */ + public function seek($position) + { + if (is_int($position) and $position > 0) { + list($node, $actual) = $this->getCollectionNode($position); + if ($actual === $position) { + $this->position = $actual; + $this->current = $node; + } else { + throw new \OutOfBoundsException( + "Unable to seek to position $position in the collection" + ); + } + } else { + throw new \InvalidArgumentException( + "Collection position must be a positive integer" + ); + } + } + + /** Rewind the iterator back to the start of the collection + * + */ + public function rewind() + { + $this->position = 1; + $this->current = null; + } + + /** Return the current item in the collection + * + * @return mixed The current item + */ + public function current() + { + if ($this->position === 1) { + return $this->get('rdf:first'); + } elseif ($this->current) { + return $this->current->get('rdf:first'); + } + } + + /** Return the key / current position in the collection + * + * Note: the first item is number 1 + * + * @return int The current position + */ + public function key() + { + return $this->position; + } + + /** Move forward to next item in the collection + * + */ + public function next() + { + if ($this->position === 1) { + $this->current = $this->get('rdf:rest'); + } elseif ($this->current) { + $this->current = $this->current->get('rdf:rest'); + } + $this->position++; + } + + /** Checks if current position is valid + * + * @return bool True if the current position is valid + */ + public function valid() + { + if ($this->position === 1 and $this->hasProperty('rdf:first')) { + return true; + } elseif ($this->current !== null and $this->current->hasProperty('rdf:first')) { + return true; + } else { + return false; + } + } + + /** Get a node for a particular offset into the collection + * + * This function may not return the item you requested, if + * it does not exist. Please check the $postion parameter + * returned. + * + * If the offset is null, then the last node in the + * collection (before rdf:nil) will be returned. + * + * @param integer $offset The offset into the collection (or null) + * + * @return array $node, $postion The node object and postion of the node + */ + public function getCollectionNode($offset) + { + $position = 1; + $node = $this; + $nil = $this->graph->resource('rdf:nil'); + while (($rest = $node->get('rdf:rest')) and $rest !== $nil and (is_null($offset) or ($position < $offset))) { + $node = $rest; + $position++; + } + return array($node, $position); + } + + /** Counts the number of items in the collection + * + * Note that this is an slow method - it is more efficient to use + * the iterator interface, if you can. + * + * @return integer The number of items in the collection + */ + public function count() + { + // Find the end of the collection + list($node, $position) = $this->getCollectionNode(null); + if (!$node->hasProperty('rdf:first')) { + return 0; + } else { + return $position; + } + } + + /** Append an item to the end of the collection + * + * @param mixed $value The value to append + * + * @return integer The number of values appended (1 or 0) + */ + public function append($value) + { + // Find the end of the collection + list($node, ) = $this->getCollectionNode(null); + $rest = $node->get('rdf:rest'); + + if ($node === $this and is_null($rest)) { + $node->set('rdf:first', $value); + $node->addResource('rdf:rest', 'rdf:nil'); + } else { + $new = $this->graph->newBnode(); + $node->set('rdf:rest', $new); + $new->add('rdf:first', $value); + $new->addResource('rdf:rest', 'rdf:nil'); + } + + return 1; + } + + /** Array Access: check if a position exists in collection using array syntax + * + * Example: isset($list[2]) + */ + public function offsetExists($offset) + { + if (is_int($offset) and $offset > 0) { + list($node, $position) = $this->getCollectionNode($offset); + return ($node and $position === $offset and $node->hasProperty('rdf:first')); + } else { + throw new \InvalidArgumentException( + "Collection offset must be a positive integer" + ); + } + } + + /** Array Access: get an item at a specified position in collection using array syntax + * + * Example: $item = $list[2]; + */ + public function offsetGet($offset) + { + if (is_int($offset) and $offset > 0) { + list($node, $position) = $this->getCollectionNode($offset); + if ($node and $position === $offset) { + return $node->get('rdf:first'); + } + } else { + throw new \InvalidArgumentException( + "Collection offset must be a positive integer" + ); + } + } + + /** + * Array Access: set an item at a positon in collection using array syntax + * + * Example: $list[2] = $item; + */ + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + // No offset - append to end of collection + $this->append($value); + } elseif (is_int($offset) and $offset > 0) { + list($node, $position) = $this->getCollectionNode($offset); + + // Create nodes, if they are missing + while ($position < $offset) { + $new = $this->graph->newBnode(); + $node->set('rdf:rest', $new); + $new->addResource('rdf:rest', 'rdf:nil'); + $node = $new; + $position++; + } + + // Terminate the list + if (!$node->hasProperty('rdf:rest')) { + $node->addResource('rdf:rest', 'rdf:nil'); + } + + return $node->set('rdf:first', $value); + } else { + throw new \InvalidArgumentException( + "Collection offset must be a positive integer" + ); + } + } + + /** + * Array Access: delete an item at a specific postion using array syntax + * + * Example: unset($seq[2]); + */ + public function offsetUnset($offset) + { + if (is_int($offset) and $offset > 0) { + list($node, $position) = $this->getCollectionNode($offset); + } else { + throw new \InvalidArgumentException( + "Collection offset must be a positive integer" + ); + } + + // Does the item exist? + if ($node and $position === $offset) { + $nil = $this->graph->resource('rdf:nil'); + if ($position === 1) { + $rest = $node->get('rdf:rest'); + if ($rest and $rest !== $nil) { + // Move second value, so we can keep the head of list + $node->set('rdf:first', $rest->get('rdf:first')); + $node->set('rdf:rest', $rest->get('rdf:rest')); + $rest->delete('rdf:first'); + $rest->delete('rdf:rest'); + } else { + // Just remove the value + $node->delete('rdf:first'); + $node->delete('rdf:rest'); + } + } else { + // Remove the value and re-link the list + $node->delete('rdf:first'); + $rest = $node->get('rdf:rest'); + $previous = $node->get('^rdf:rest'); + if (is_null($rest)) { + $rest = $nil; + } + if ($previous) { + $previous->set('rdf:rest', $rest); + } + } + } + } +} diff --git a/lib/Container.php b/lib/Container.php new file mode 100644 index 0000000..75149c1 --- /dev/null +++ b/lib/Container.php @@ -0,0 +1,234 @@ +position = 1; + parent::__construct($uri, $graph); + } + + /** Seek to a specific position in the container + * + * The first item is postion 1 + * + * @param integer $position The position in the container to seek to + * + * @throws \OutOfBoundsException + * @throws \InvalidArgumentException + */ + public function seek($position) + { + if (is_int($position) and $position > 0) { + if ($this->hasProperty('rdf:_'.$position)) { + $this->position = $position; + } else { + throw new \OutOfBoundsException( + "Unable to seek to position $position in the container" + ); + } + } else { + throw new \InvalidArgumentException( + "Container position must be a positive integer" + ); + } + } + + /** Rewind the iterator back to the start of the container (item 1) + * + */ + public function rewind() + { + $this->position = 1; + } + + /** Return the current item in the container + * + * @return mixed The current item + */ + public function current() + { + return $this->get('rdf:_'.$this->position); + } + + /** Return the key / current position in the container + * + * @return int The current position + */ + public function key() + { + return $this->position; + } + + /** Move forward to next item in the container + * + */ + public function next() + { + $this->position++; + } + + /** Checks if current position is valid + * + * @return bool True if the current position is valid + */ + public function valid() + { + return $this->hasProperty('rdf:_'.$this->position); + } + + /** Counts the number of items in the container + * + * Note that this is an slow method - it is more efficient to use + * the iterator interface, if you can. + * + * @return integer The number of items in the container + */ + public function count() + { + $pos = 1; + while ($this->hasProperty('rdf:_'.$pos)) { + $pos++; + } + return $pos - 1; + } + + /** Append an item to the end of the container + * + * @param mixed $value The value to append + * + * @return integer The number of values appended (1 or 0) + */ + public function append($value) + { + // Find the end of the list + $pos = 1; + while ($this->hasProperty('rdf:_'.$pos)) { + $pos++; + } + + // Add the item + return $this->add('rdf:_'.$pos, $value); + } + + /** Array Access: check if a position exists in container using array syntax + * + * Example: isset($seq[2]) + */ + public function offsetExists($offset) + { + if (is_int($offset) and $offset > 0) { + return $this->hasProperty('rdf:_'.$offset); + } else { + throw new \InvalidArgumentException( + "Container position must be a positive integer" + ); + } + } + + /** Array Access: get an item at a specified position in container using array syntax + * + * Example: $item = $seq[2]; + */ + public function offsetGet($offset) + { + if (is_int($offset) and $offset > 0) { + return $this->get('rdf:_'.$offset); + } else { + throw new \InvalidArgumentException( + "Container position must be a positive integer" + ); + } + } + + /** + * Array Access: set an item at a positon in container using array syntax + * + * Example: $seq[2] = $item; + * + * Warning: creating gaps in the sequence will result in unexpected behavior + */ + public function offsetSet($offset, $value) + { + if (is_int($offset) and $offset > 0) { + return $this->set('rdf:_'.$offset, $value); + } elseif (is_null($offset)) { + return $this->append($value); + } else { + throw new \InvalidArgumentException( + "Container position must be a positive integer" + ); + } + } + + /** + * Array Access: delete an item at a specific postion using array syntax + * + * Example: unset($seq[2]); + * + * Warning: creating gaps in the sequence will result in unexpected behavior + */ + public function offsetUnset($offset) + { + if (is_int($offset) and $offset > 0) { + return $this->delete('rdf:_'.$offset); + } else { + throw new \InvalidArgumentException( + "Container position must be a positive integer" + ); + } + } +} diff --git a/lib/Exception.php b/lib/Exception.php new file mode 100644 index 0000000..9aa8458 --- /dev/null +++ b/lib/Exception.php @@ -0,0 +1,51 @@ + 0.5) where 0.5 is the + * q value for that type. The types are sorted by q value + * before constructing the string. + * + * @param array $extraTypes extra MIME types to add + * + * @return string list of supported MIME types + */ + public static function getHttpAcceptHeader(array $extraTypes = array()) + { + $accept = $extraTypes; + foreach (self::$formats as $format) { + if ($format->parserClass and count($format->mimeTypes) > 0) { + $accept = array_merge($accept, $format->mimeTypes); + } + } + + return self::formatAcceptHeader($accept); + } + + /** + * Convert array of types to Accept header value + * @param array $accepted_types + * @return string + */ + public static function formatAcceptHeader(array $accepted_types) + { + arsort($accepted_types, SORT_NUMERIC); + + $acceptStr = ''; + foreach ($accepted_types as $type => $q) { + if ($acceptStr) { + $acceptStr .= ','; + } + if ($q == 1.0) { + $acceptStr .= $type; + } else { + $acceptStr .= sprintf("%s;q=%1.1F", $type, $q); + } + } + + return $acceptStr; + } + + /** Check if a named graph exists + * + * @param string $name the name of the format + * + * @return boolean true if the format exists + */ + public static function formatExists($name) + { + return array_key_exists($name, self::$formats); + } + + /** Get a EasyRdf\Format from a name, uri or mime type + * + * @param string $query a query string to search for + * + * @return self the first object that matches the query + * @throws \InvalidArgumentException + * @throws Exception if no format is found + */ + public static function getFormat($query) + { + if (!is_string($query) or $query == null or $query == '') { + throw new \InvalidArgumentException( + "\$query should be a string and cannot be null or empty" + ); + } + + foreach (self::$formats as $format) { + if ($query == $format->name or + $query == $format->uri or + array_key_exists($query, $format->mimeTypes) or + in_array($query, $format->extensions)) { + return $format; + } + } + + # No match + throw new Exception( + "Format is not recognised: $query" + ); + } + + /** Register a new format + * + * @param string $name The name of the format (e.g. ntriples) + * @param string $label The label for the format (e.g. N-Triples) + * @param string $uri The URI for the format + * @param array|string $mimeTypes One or more mime types for the format + * @param array|string $extensions One or more extensions (file suffix) + * + * @throws \InvalidArgumentException + * @return self New Format object + */ + public static function register( + $name, + $label = null, + $uri = null, + $mimeTypes = array(), + $extensions = array() + ) { + if (!is_string($name) or $name == null or $name == '') { + throw new \InvalidArgumentException( + "\$name should be a string and cannot be null or empty" + ); + } + + if (!array_key_exists($name, self::$formats)) { + self::$formats[$name] = new self($name); + } + + self::$formats[$name]->setLabel($label); + self::$formats[$name]->setUri($uri); + self::$formats[$name]->setMimeTypes($mimeTypes); + self::$formats[$name]->setExtensions($extensions); + return self::$formats[$name]; + } + + /** Remove a format from the registry + * + * @param string $name The name of the format (e.g. ntriples) + */ + public static function unregister($name) + { + unset(self::$formats[$name]); + } + + /** Class method to register a parser class to a format name + * + * @param string $name The name of the format (e.g. ntriples) + * @param string $class The name of the class (e.g. EasyRdf\Parser\Ntriples) + */ + public static function registerParser($name, $class) + { + if (!self::formatExists($name)) { + self::register($name); + } + self::getFormat($name)->setParserClass($class); + } + + /** Class method to register a serialiser class to a format name + * + * @param string $name The name of the format (e.g. ntriples) + * @param string $class The name of the class (e.g. EasyRdf\Serialiser\Ntriples) + */ + public static function registerSerialiser($name, $class) + { + if (!self::formatExists($name)) { + self::register($name); + } + self::getFormat($name)->setSerialiserClass($class); + } + + /** Attempt to guess the document format from some content. + * + * If $filename is given, then the suffix is first used to guess the format. + * + * If the document format is not recognised, null is returned. + * + * @param string $data The document data + * @param string $filename Optional filename + * + * @return self New format object + */ + public static function guessFormat($data, $filename = null) + { + if (is_array($data)) { + # Data has already been parsed into RDF/PHP + return self::getFormat('php'); + } + + // First try and identify by the filename + if ($filename and preg_match('/\.(\w+)$/', $filename, $matches)) { + foreach (self::$formats as $format) { + if (in_array($matches[1], $format->extensions)) { + return $format; + } + } + } + + // Then try and guess by the first 1024 bytes of content + $short = substr($data, 0, 1024); + if (preg_match('/^\s*\{/', $short)) { + return self::getFormat('json'); + } elseif (preg_match('/ <.+>/m', $short)) { + return self::getFormat('ntriples'); + } else { + return null; + } + } + + /** + * This constructor is for internal use only. + * To create a new format, use the register method. + * + * @param string $name The name of the format + * @see Format::register() + * @ignore + */ + public function __construct($name) + { + $this->name = $name; + $this->label = $name; # Only a default + } + + /** Get the name of a format object + * + * @return string The name of the format (e.g. rdfxml) + */ + public function getName() + { + return $this->name; + } + + /** Get the label for a format object + * + * @return string The format label (e.g. RDF/XML) + */ + public function getLabel() + { + return $this->label; + } + + /** Set the label for a format object + * + * @param string $label The new label for the format + * + * @throws \InvalidArgumentException + * @return string|null + */ + public function setLabel($label) + { + if ($label) { + if (!is_string($label)) { + throw new \InvalidArgumentException( + "\$label should be a string" + ); + } + return $this->label = $label; + } else { + return $this->label = null; + } + } + + /** Get the URI for a format object + * + * @return string The format URI + */ + public function getUri() + { + return $this->uri; + } + + /** Set the URI for a format object + * + * @param string $uri The new URI for the format + * + * @throws \InvalidArgumentException + * @return string|null + */ + public function setUri($uri) + { + if ($uri) { + if (!is_string($uri)) { + throw new \InvalidArgumentException( + "\$uri should be a string" + ); + } + return $this->uri = $uri; + } else { + return $this->uri = null; + } + } + + /** Get the default registered mime type for a format object + * + * @return string The default mime type as a string. + */ + public function getDefaultMimeType() + { + $types = array_keys($this->mimeTypes); + if (isset($types[0])) { + return $types[0]; + } + } + + /** Get all the registered mime types for a format object + * + * @return array One or more MIME types in an array with + * the mime type as the key and q value as the value + */ + public function getMimeTypes() + { + return $this->mimeTypes; + } + + /** Set the MIME Types for a format object + * + * @param string|array $mimeTypes One or more mime types + */ + public function setMimeTypes($mimeTypes) + { + if ($mimeTypes) { + if (!is_array($mimeTypes)) { + $mimeTypes = array($mimeTypes); + } + $this->mimeTypes = $mimeTypes; + } else { + $this->mimeTypes = array(); + } + } + + /** Get the default registered file extension (filename suffix) for a format object + * + * @return string The default extension as a string. + */ + public function getDefaultExtension() + { + if (isset($this->extensions[0])) { + return $this->extensions[0]; + } + } + + /** Get all the registered file extensions (filename suffix) for a format object + * + * @return array One or more extensions as an array + */ + public function getExtensions() + { + return $this->extensions; + } + + /** Set the file format extensions (filename suffix) for a format object + * + * @param mixed $extensions One or more file extensions + */ + public function setExtensions($extensions) + { + if ($extensions) { + if (!is_array($extensions)) { + $extensions = array($extensions); + } + $this->extensions = $extensions; + } else { + $this->extensions = array(); + } + } + + /** Set the parser to use for a format + * + * @param string $class The name of the class + * + * @throws \InvalidArgumentException + */ + public function setParserClass($class) + { + if ($class) { + if (!is_string($class)) { + throw new \InvalidArgumentException( + "\$class should be a string" + ); + } + $this->parserClass = $class; + } else { + $this->parserClass = null; + } + } + + /** Get the name of the class to use to parse the format + * + * @return string The name of the class + */ + public function getParserClass() + { + return $this->parserClass; + } + + /** Create a new parser to parse this format + * + * @throws Exception + * @return object The new parser object + */ + public function newParser() + { + $parserClass = $this->parserClass; + if (!$parserClass) { + throw new Exception( + "No parser class available for format: ".$this->getName() + ); + } + return (new $parserClass()); + } + + /** Set the serialiser to use for a format + * + * @param string $class The name of the class + * + * @throws \InvalidArgumentException + */ + public function setSerialiserClass($class) + { + if ($class) { + if (!is_string($class)) { + throw new \InvalidArgumentException( + "\$class should be a string" + ); + } + $this->serialiserClass = $class; + } else { + $this->serialiserClass = null; + } + } + + /** Get the name of the class to use to serialise the format + * + * @return string The name of the class + */ + public function getSerialiserClass() + { + return $this->serialiserClass; + } + + /** Create a new serialiser to parse this format + * + * @throws Exception + * @return object The new serialiser object + */ + public function newSerialiser() + { + $serialiserClass = $this->serialiserClass; + if (!$serialiserClass) { + throw new Exception( + "No serialiser class available for format: ".$this->getName() + ); + } + return (new $serialiserClass()); + } + + /** Magic method to return the name of the format when casted to string + * + * @return string The name of the format + */ + public function __toString() + { + return $this->name; + } +} + + +/* + Register default set of supported formats + NOTE: they are ordered by preference +*/ + +Format::register( + 'php', + 'RDF/PHP', + 'https://www.easyrdf.org/docs/rdf-formats-php', + array( + 'application/x-httpd-php-source' => 1.0 + ), + array('phps') +); + +Format::register( + 'json', + 'RDF/JSON Resource-Centric', + 'https://www.easyrdf.org/docs/rdf-formats-json', + array( + 'application/json' => 1.0, + 'text/json' => 0.9, + 'application/rdf+json' => 0.9 + ), + array('json') +); + +Format::register( + 'jsonld', + 'JSON-LD', + 'http://www.w3.org/TR/json-ld/', + array( + 'application/ld+json' => 1.0 + ), + array('jsonld') +); + +Format::register( + 'ntriples', + 'N-Triples', + 'http://www.w3.org/TR/n-triples/', + array( + 'application/n-triples' => 1.0, + 'text/plain' => 0.9, + 'text/ntriples' => 0.9, + 'application/ntriples' => 0.9, + 'application/x-ntriples' => 0.9 + ), + array('nt') +); + +Format::register( + 'turtle', + 'Turtle Terse RDF Triple Language', + 'https://www.w3.org/TR/turtle/', + array( + 'text/turtle' => 0.8, + 'application/turtle' => 0.7, + 'application/x-turtle' => 0.7 + ), + array('ttl') +); + +Format::register( + 'rdfxml', + 'RDF/XML', + 'http://www.w3.org/TR/rdf-syntax-grammar/', + array( + 'application/rdf+xml' => 0.8, + 'text/xml' => 0.5, + 'application/xml' => 0.5 + ), + array('rdf', 'xrdf') +); + +Format::register( + 'dot', + 'Graphviz', + 'http://www.graphviz.org/doc/info/lang.html', + array( + 'text/vnd.graphviz' => 0.8 + ), + array('gv', 'dot') +); + +Format::register( + 'json-triples', + 'RDF/JSON Triples' +); + +Format::register( + 'n3', + 'Notation3', + 'http://www.w3.org/2000/10/swap/grammar/n3#', + array( + 'text/n3' => 0.5, + 'text/rdf+n3' => 0.5 + ), + array('n3') +); + +Format::register( + 'rdfa', + 'RDFa', + 'http://www.w3.org/TR/rdfa-core/', + array( + 'text/html' => 0.4, + 'application/xhtml+xml' => 0.4 + ), + array('html') +); + +Format::register( + 'sparql-xml', + 'SPARQL XML Query Results', + 'http://www.w3.org/TR/rdf-sparql-XMLres/', + array( + 'application/sparql-results+xml' => 1.0 + ) +); + +Format::register( + 'sparql-json', + 'SPARQL JSON Query Results', + 'http://www.w3.org/TR/rdf-sparql-json-res/', + array( + 'application/sparql-results+json' => 1.0 + ) +); + +Format::register( + 'png', + 'Portable Network Graphics (PNG)', + 'http://www.w3.org/TR/PNG/', + array( + 'image/png' => 0.3 + ), + array('png') +); + +Format::register( + 'gif', + 'Graphics Interchange Format (GIF)', + 'http://www.w3.org/Graphics/GIF/spec-gif89a.txt', + array( + 'image/gif' => 0.2 + ), + array('gif') +); + +Format::register( + 'svg', + 'Scalable Vector Graphics (SVG)', + 'http://www.w3.org/TR/SVG/', + array( + 'image/svg+xml' => 0.3 + ), + array('svg') +); + + +/* + Register default set of parsers and serialisers +*/ + +Format::registerParser('json', 'EasyRdf\Parser\Json'); +Format::registerParser('jsonld', 'EasyRdf\Parser\JsonLd'); +Format::registerParser('ntriples', 'EasyRdf\Parser\Ntriples'); +Format::registerParser('php', 'EasyRdf\Parser\RdfPhp'); +Format::registerParser('rdfxml', 'EasyRdf\Parser\RdfXml'); +Format::registerParser('turtle', 'EasyRdf\Parser\Turtle'); +Format::registerParser('rdfa', 'EasyRdf\Parser\Rdfa'); + +Format::registerSerialiser('json', 'EasyRdf\Serialiser\Json'); +Format::registerSerialiser('jsonld', 'EasyRdf\Serialiser\JsonLd'); +Format::registerSerialiser('n3', 'EasyRdf\Serialiser\Turtle'); +Format::registerSerialiser('ntriples', 'EasyRdf\Serialiser\Ntriples'); +Format::registerSerialiser('php', 'EasyRdf\Serialiser\RdfPhp'); +Format::registerSerialiser('rdfxml', 'EasyRdf\Serialiser\RdfXml'); +Format::registerSerialiser('turtle', 'EasyRdf\Serialiser\Turtle'); + +Format::registerSerialiser('dot', 'EasyRdf\Serialiser\GraphViz'); +Format::registerSerialiser('gif', 'EasyRdf\Serialiser\GraphViz'); +Format::registerSerialiser('png', 'EasyRdf\Serialiser\GraphViz'); +Format::registerSerialiser('svg', 'EasyRdf\Serialiser\GraphViz'); diff --git a/lib/Graph.php b/lib/Graph.php new file mode 100644 index 0000000..e891a5a --- /dev/null +++ b/lib/Graph.php @@ -0,0 +1,1753 @@ +checkResourceParam($uri, true); + + if ($uri) { + $this->uri = $uri; + $this->parsedUri = new ParsedUri($uri); + if ($data) { + $this->parse($data, $format, $this->uri); + } + } + } + + /** + * Create a new graph and load RDF data from a URI into it + * + * This static function is shorthand for: + * $graph = new \EasyRdf\Graph($uri); + * $graph->load($uri, $format); + * + * The document format is optional but should be specified if it + * can't be guessed or got from the HTTP headers. + * + * If the document format is given, then the HTTP Accept header is + * set to the MIME type of the requested format. + * + * @param string $uri The URI of the data to load + * @param string|null $format Optional format of the data (eg. rdfxml or text/turtle) + * + * @return Graph The new the graph object + */ + public static function newAndLoad($uri, $format = null) + { + $graph = new self($uri); + $graph->load($uri, $format); + return $graph; + } + + /** Get or create a resource stored in a graph + * + * If the resource did not previously exist, then a new resource will + * be created. If you provide an RDF type and that type is registered + * with the EasyRdf\TypeMapper, then the resource will be an instance + * of the class registered. + * + * If URI is null, then the URI of the graph is used. + * + * @param string $uri The URI of the resource + * @param mixed $types RDF type of a new resource (e.g. foaf:Person) + * + * @throws \InvalidArgumentException + * @return \EasyRdf\Resource + */ + public function resource($uri = null, $types = array()) + { + $this->checkResourceParam($uri, true); + if (!$uri) { + throw new \InvalidArgumentException( + '$uri is null and EasyRdf\Graph object has no URI either.' + ); + } + + // Resolve relative URIs + if ($this->parsedUri) { + $uri = $this->parsedUri->resolve($uri)->toString(); + } + + // Add the types + $this->addType($uri, $types); + + // Create resource object if it doesn't already exist + if (!isset($this->resources[$uri])) { + $resClass = $this->classForResource($uri); + $this->resources[$uri] = new $resClass($uri, $this); + } + + return $this->resources[$uri]; + } + + /** Work out the class to instantiate a resource as + * @ignore + */ + protected function classForResource($uri) + { + $rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; + if (isset($this->index[$uri][$rdfType])) { + foreach ($this->index[$uri][$rdfType] as $type) { + if ($type['type'] == 'uri' or $type['type'] == 'bnode') { + $class = TypeMapper::get($type['value']); + if ($class != null) { + return $class; + } + } + } + } + + // Parsers don't typically add a rdf:type to rdf:List, so we have to + // do a bit of 'inference' here using properties. + if ($uri == 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' or + isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#first']) or + isset($this->index[$uri]['http://www.w3.org/1999/02/22-rdf-syntax-ns#rest']) + ) { + return 'EasyRdf\Collection'; + } + return TypeMapper::getDefaultResourceClass(); + } + + /** + * Create a new blank node in the graph and return it. + * + * If you provide an RDF type and that type is registered + * with the EasyRdf\TypeMapper, then the resource will be an instance + * of the class registered. + * + * @param mixed $types RDF type of a new blank node (e.g. foaf:Person) + * + * @return \EasyRdf\Resource The new blank node + */ + public function newBNode($types = array()) + { + return $this->resource($this->newBNodeId(), $types); + } + + /** + * Create a new unique blank node identifier and return it. + * + * @return string The new blank node identifier (e.g. _:genid1) + */ + public function newBNodeId() + { + return "_:genid".(++$this->bNodeCount); + } + + /** + * Parse some RDF data into the graph object. + * + * @param string $data Data to parse for the graph + * @param string $format Optional format of the data + * @param string $uri The URI of the data to load + * + * @throws Exception + * @return integer The number of triples added to the graph + */ + public function parse($data, $format = null, $uri = null) + { + $this->checkResourceParam($uri, true); + + if (empty($format) or $format == 'guess') { + // Guess the format if it is Unknown + $format = Format::guessFormat($data, $uri); + } else { + $format = Format::getFormat($format); + } + + if (!$format) { + throw new Exception( + "Unable to parse data of an unknown format." + ); + } + + $parser = $format->newParser(); + return $parser->parse($this, $data, $format, $uri); + } + + /** + * Parse a file containing RDF data into the graph object. + * + * @param string $filename The path of the file to load + * @param string $format Optional format of the file + * @param string $uri The URI of the file to load + * + * @return integer The number of triples added to the graph + */ + public function parseFile($filename, $format = null, $uri = null) + { + if ($uri === null) { + $uri = "file://$filename"; + } + + return $this->parse( + file_get_contents($filename), + $format, + $uri + ); + } + + /** + * Load RDF data into the graph from a URI. + * + * If no URI is given, then the URI of the graph will be used. + * + * The document format is optional but should be specified if it + * can't be guessed or got from the HTTP headers. + * + * If the document format is given, then the HTTP Accept header is + * set to the MIME type of the requested format. + * + * @param string $uri The URI of the data to load + * @param string $format Optional format of the data (eg. rdfxml or text/turtle) + * + * @throws Exception + * @throws Http\Exception + * @return integer The number of triples added to the graph + */ + public function load($uri = null, $format = null) + { + $this->checkResourceParam($uri, true); + + if (!$uri) { + throw new Exception( + "No URI given to load() and the graph does not have a URI." + ); + } + + // Setup the HTTP client + $client = Http::getDefaultHttpClient(); + $client->resetParameters(true); + $client->setConfig(array('maxredirects' => 0)); + $client->setMethod('GET'); + + if ($format && $format !== 'guess') { + if (strpos($format, '/') !== false) { + $client->setHeaders('Accept', $format); + } else { + $formatObj = Format::getFormat($format); + $client->setHeaders('Accept', $formatObj->getDefaultMimeType()); + } + } else { + // Send a list of all the formats we can parse + $client->setHeaders('Accept', Format::getHttpAcceptHeader()); + } + + $requestUrl = $uri; + $response = null; + $redirectCounter = 0; + do { + // Have we already loaded it into the graph? + $requestUrl = Utils::removeFragmentFromUri($requestUrl); + if (in_array($requestUrl, $this->loaded)) { + return 0; + } + + // Make the HTTP request + $client->setHeaders('host', null); + $client->setUri($requestUrl); + $response = $client->request(); + + // Add the URL to the list of URLs loaded + $this->loaded[] = $requestUrl; + + if ($response->isRedirect() and $location = $response->getHeader('location')) { + // Avoid problems with buggy servers that add whitespace + $location = trim($location); + + // Some servers return relative URLs in the location header + // resolve it in relation to previous request + $baseUri = new ParsedUri($requestUrl); + $requestUrl = $baseUri->resolve($location)->toString(); + $requestUrl = Utils::removeFragmentFromUri($requestUrl); + + // If it is a 303 then drop the parameters + if ($response->getStatus() == 303) { + $client->resetParameters(); + } + + ++$redirectCounter; + } elseif ($response->isSuccessful()) { + // If we didn't get any location, stop redirecting + break; + } else { + throw new Http\Exception( + "HTTP request for {$requestUrl} failed: ".$response->getMessage(), + $response->getStatus(), + null, + $response->getBody() + ); + } + } while ($redirectCounter < $this->maxRedirects); + + if (!$format or $format == 'guess') { + list($format, ) = Utils::parseMimeType( + $response->getHeader('Content-Type') + ); + } + + // Parse the data + return $this->parse($response->getBody(), $format, $uri); + } + + /** Get an associative array of all the resources stored in the graph. + * The keys of the array is the URI of the EasyRdf\Resource. + * + * @return Resource[] + */ + public function resources() + { + foreach ($this->index as $subject => $properties) { + if (!isset($this->resources[$subject])) { + $this->resource($subject); + } + } + + foreach ($this->revIndex as $object => $properties) { + if (!isset($this->resources[$object])) { + $this->resource($object); + } + } + + return $this->resources; + } + + /** Get an arry of resources matching a certain property and optional value. + * + * For example this routine could be used as a way of getting + * everyone who has name: + * $people = $graph->resourcesMatching('foaf:name') + * + * Or everyone who is male: + * $people = $graph->resourcesMatching('foaf:gender', 'male'); + * + * Or all homepages: + * $people = $graph->resourcesMatching('^foaf:homepage'); + * + * @param string $property The property to check. + * @param mixed $value Optional, the value of the propery to check for. + * + * @return Resource[] + */ + public function resourcesMatching($property, $value = null) + { + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + // Use the reverse index if it is an inverse property + if ($inverse) { + $index = &$this->revIndex; + } else { + $index = &$this->index; + } + + $matched = array(); + foreach ($index as $subject => $props) { + if (isset($index[$subject][$property])) { + if (isset($value)) { + foreach ($this->index[$subject][$property] as $v) { + if ($v['type'] == $value['type'] and + $v['value'] == $value['value']) { + $matched[] = $this->resource($subject); + break; + } + } + } else { + $matched[] = $this->resource($subject); + } + } + } + return $matched; + } + + /** Get the URI of the graph + * + * @return string The URI of the graph + */ + public function getUri() + { + return $this->uri; + } + + /** Check that a URI/resource parameter is valid, and convert it to a string + * @ignore + */ + protected function checkResourceParam(&$resource, $allowNull = false) + { + if ($allowNull == true) { + if ($resource === null) { + if ($this->uri) { + $resource = $this->uri; + } else { + return; + } + } + } elseif ($resource === null) { + throw new \InvalidArgumentException( + '$resource should be either IRI, blank-node identifier or EasyRdf\Resource. got null' + ); + } + + if (is_object($resource) and $resource instanceof Resource) { + $resource = $resource->getUri(); + } elseif (is_object($resource) and $resource instanceof ParsedUri) { + $resource = strval($resource); + } elseif (is_string($resource)) { + if ($resource == '') { + throw new \InvalidArgumentException( + '$resource should be either IRI, blank-node identifier or EasyRdf\Resource. got empty string' + ); + } elseif (preg_match("|^<(.+)>$|", $resource, $matches)) { + $resource = $matches[1]; + } else { + $resource = RdfNamespace::expand($resource); + } + } else { + throw new \InvalidArgumentException( + '$resource should be either IRI, blank-node identifier or EasyRdf\Resource' + ); + } + } + + /** Check that a single URI/property parameter (not a property path) + * is valid, and expand it if required + * @ignore + */ + protected function checkSinglePropertyParam(&$property, &$inverse) + { + if (is_object($property) and $property instanceof Resource) { + $property = $property->getUri(); + } elseif (is_object($property) and $property instanceof ParsedUri) { + $property = strval($property); + } elseif (is_string($property)) { + if ($property == '') { + throw new \InvalidArgumentException( + "\$property cannot be an empty string" + ); + } elseif (substr($property, 0, 1) == '^') { + $inverse = true; + $property = RdfNamespace::expand(substr($property, 1)); + } elseif (substr($property, 0, 2) == '_:') { + throw new \InvalidArgumentException( + "\$property cannot be a blank node" + ); + } else { + $inverse = false; + $property = RdfNamespace::expand($property); + } + } + + if ($property === null or !is_string($property)) { + throw new \InvalidArgumentException( + '$property should be a string or EasyRdf\Resource and cannot be null' + ); + } + } + + /** Check that a value parameter is valid, and convert it to an associative array if needed + * @ignore + */ + protected function checkValueParam(&$value) + { + if (isset($value)) { + if (is_object($value)) { + if (!method_exists($value, 'toRdfPhp')) { + // Convert to a literal object + $value = Literal::create($value); + } + $value = $value->toRdfPhp(); + } elseif (is_array($value)) { + if (!isset($value['type'])) { + throw new \InvalidArgumentException( + "\$value is missing a 'type' key" + ); + } + + if (!isset($value['value'])) { + throw new \InvalidArgumentException( + "\$value is missing a 'value' key" + ); + } + + // Fix ordering and remove unknown keys + $value = array( + 'type' => strval($value['type']), + 'value' => strval($value['value']), + 'lang' => isset($value['lang']) ? strval($value['lang']) : null, + 'datatype' => isset($value['datatype']) ? strval($value['datatype']) : null + ); + } else { + $value = array( + 'type' => 'literal', + 'value' => strval($value), + 'datatype' => Literal::getDatatypeForValue($value) + ); + } + if (!in_array($value['type'], array('uri', 'bnode', 'literal'), true)) { + throw new \InvalidArgumentException( + "\$value does not have a valid type (".$value['type'].")" + ); + } + if (empty($value['datatype'])) { + unset($value['datatype']); + } + if (empty($value['lang'])) { + unset($value['lang']); + } + if (isset($value['lang']) and isset($value['datatype'])) { + throw new \InvalidArgumentException( + "\$value cannot have both and language and a datatype" + ); + } + } + } + + /** Get a single value for a property of a resource + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * If $property is an array, then the first item in the array that matches + * a property that exists is returned. + * + * This method will return null if the property does not exist. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $propertyPath A valid property path + * @param string $type The type of value to filter by (e.g. literal or resource) + * @param string $lang The language to filter by (e.g. en) + * + * @throws \InvalidArgumentException + * @return mixed A value associated with the property + */ + public function get($resource, $propertyPath, $type = null, $lang = null) + { + $this->checkResourceParam($resource); + + if (is_object($propertyPath) and $propertyPath instanceof Resource) { + return $this->getSingleProperty($resource, $propertyPath->getUri(), $type, $lang); + } elseif (is_string($propertyPath) and preg_match('|^(\^?)<(.+)>|', $propertyPath, $matches)) { + return $this->getSingleProperty($resource, "$matches[1]$matches[2]", $type, $lang); + } elseif ($propertyPath === null or !is_string($propertyPath)) { + throw new \InvalidArgumentException( + '$propertyPath should be a string or EasyRdf\Resource and cannot be null' + ); + } elseif ($propertyPath === '') { + throw new \InvalidArgumentException( + "\$propertyPath cannot be an empty string" + ); + } + + // Loop through each component in the path + foreach (explode('/', $propertyPath) as $part) { + // Stop if we come to a literal + if ($resource instanceof Literal) { + return null; + } + + // Try each of the alternative paths + foreach (explode('|', $part) as $p) { + $res = $this->getSingleProperty($resource, $p, $type, $lang); + if ($res) { + break; + } + } + + // Stop if nothing was found + $resource = $res; + if (!$resource) { + break; + } + } + + return $resource; + } + + /** Get a single value for a property of a resource + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal or resource) + * @param string $lang The language to filter by (e.g. en) + * + * @return mixed A value associated with the property + * + * @ignore + */ + protected function getSingleProperty($resource, $property, $type = null, $lang = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + + // Get an array of values for the property + $values = $this->propertyValuesArray($resource, $property, $inverse); + if (!isset($values)) { + return null; + } + + // Filter the results + $result = null; + if ($type) { + foreach ($values as $value) { + if ($type == 'literal' and $value['type'] == 'literal') { + if ($lang == null or (isset($value['lang']) and $value['lang'] == $lang)) { + $result = $value; + break; + } + } elseif ($type == 'resource') { + if ($value['type'] == 'uri' or $value['type'] == 'bnode') { + $result = $value; + break; + } + } + } + } else { + $result = $values[0]; + } + + // Convert the internal data structure into a PHP object + return $this->arrayToObject($result); + } + + /** Get a single literal value for a property of a resource + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * This method will return null if there is not literal value for the + * property. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string|array $property The name of the property (e.g. foaf:name) + * @param string $lang The language to filter by (e.g. en) + * + * @return Literal Literal value associated with the property + */ + public function getLiteral($resource, $property, $lang = null) + { + return $this->get($resource, $property, 'literal', $lang); + } + + /** Get a single resource value for a property of a resource + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * This method will return null if there is not resource for the + * property. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string|array $property The name of the property (e.g. foaf:name) + * + * @return \EasyRdf\Resource Resource associated with the property + */ + public function getResource($resource, $property) + { + return $this->get($resource, $property, 'resource'); + } + + /** Return all the values for a particular property of a resource + * @ignore + */ + protected function propertyValuesArray($resource, $property, $inverse = false) + { + // Is an inverse property being requested? + if ($inverse) { + if (isset($this->revIndex[$resource])) { + $properties = &$this->revIndex[$resource]; + } + } else { + if (isset($this->index[$resource])) { + $properties = &$this->index[$resource]; + } + } + + if (isset($properties[$property])) { + return $properties[$property]; + } else { + return null; + } + } + + /** Get an EasyRdf\Resource or EasyRdf\Literal object from an associative array. + * @ignore + */ + protected function arrayToObject($data) + { + if ($data) { + if ($data['type'] == 'uri' or $data['type'] == 'bnode') { + return $this->resource($data['value']); + } else { + return Literal::create($data); + } + } else { + return null; + } + } + + /** Get all values for a property path + * + * This method will return an empty array if the property does not exist. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $propertyPath A valid property path + * @param string $type The type of value to filter by (e.g. literal) + * @param string $lang The language to filter by (e.g. en) + * + * @throws \InvalidArgumentException + * @return array An array of values associated with the property + */ + public function all($resource, $propertyPath, $type = null, $lang = null) + { + $this->checkResourceParam($resource); + + if (is_object($propertyPath) and $propertyPath instanceof Resource) { + return $this->allForSingleProperty($resource, $propertyPath->getUri(), $type, $lang); + } elseif (is_string($propertyPath) and preg_match('|^(\^?)<(.+)>|', $propertyPath, $matches)) { + return $this->allForSingleProperty($resource, "$matches[1]$matches[2]", $type, $lang); + } elseif ($propertyPath === null or !is_string($propertyPath)) { + throw new \InvalidArgumentException( + '$propertyPath should be a string or EasyRdf\Resource and cannot be null' + ); + } elseif ($propertyPath === '') { + throw new \InvalidArgumentException( + "\$propertyPath cannot be an empty string" + ); + } + + $objects = array($resource); + + // Loop through each component in the path + foreach (explode('/', $propertyPath) as $part) { + $results = array(); + foreach (explode('|', $part) as $p) { + foreach ($objects as $o) { + // Ignore literals found earlier in path + if ($o instanceof Literal) { + continue; + } + + $results = array_merge( + $results, + $this->allForSingleProperty($o, $p, $type, $lang) + ); + } + } + + // Stop if we don't have anything + if (empty($objects)) { + break; + } + + // Use the results as the input to the next iteration + $objects = $results; + } + + return $results; + } + + /** Get all values for a single property of a resource + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal) + * @param string $lang The language to filter by (e.g. en) + * + * @return array An array of values associated with the property + * + * @ignore + */ + protected function allForSingleProperty($resource, $property, $type = null, $lang = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + + // Get an array of values for the property + $values = $this->propertyValuesArray($resource, $property, $inverse); + if (!isset($values)) { + return array(); + } + + $objects = array(); + if ($type) { + foreach ($values as $value) { + if ($type == 'literal' and $value['type'] == 'literal') { + if ($lang == null or (isset($value['lang']) and $value['lang'] == $lang)) { + $objects[] = $this->arrayToObject($value); + } + } elseif ($type == 'resource') { + if ($value['type'] == 'uri' or $value['type'] == 'bnode') { + $objects[] = $this->arrayToObject($value); + } + } + } + } else { + foreach ($values as $value) { + $objects[] = $this->arrayToObject($value); + } + } + return $objects; + } + + /** Get all literal values for a property of a resource + * + * This method will return an empty array if the resource does not + * has any literal values for that property. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $property The name of the property (e.g. foaf:name) + * @param string $lang The language to filter by (e.g. en) + * + * @return array An array of values associated with the property + */ + public function allLiterals($resource, $property, $lang = null) + { + return $this->all($resource, $property, 'literal', $lang); + } + + /** Get all resources for a property of a resource + * + * This method will return an empty array if the resource does not + * has any resources for that property. + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $property The name of the property (e.g. foaf:name) + * + * @return array An array of values associated with the property + */ + public function allResources($resource, $property) + { + return $this->all($resource, $property, 'resource'); + } + + /** Get all the resources in the graph of a certain type + * + * If no resources of the type are available and empty + * array is returned. + * + * @param string $type The type of the resource (e.g. foaf:Person) + * + * @return array The array of resources + */ + public function allOfType($type) + { + return $this->all($type, '^rdf:type'); + } + + /** Count the number of values for a property of a resource + * + * @param string $resource The URI of the resource (e.g. http://example.com/joe#me) + * @param string $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal) + * @param string $lang The language to filter by (e.g. en) + * + * @return integer The number of values for this property + */ + public function countValues($resource, $property, $type = null, $lang = null) + { + return count($this->all($resource, $property, $type, $lang)); + } + + /** Concatenate all values for a property of a resource into a string. + * + * The default is to join the values together with a space character. + * This method will return an empty string if the property does not exist. + * + * @param mixed $resource The resource to get the property on + * @param string $property The name of the property (e.g. foaf:name) + * @param string $glue The string to glue the values together with. + * @param string $lang The language to filter by (e.g. en) + * + * @return string Concatenation of all the values. + */ + public function join($resource, $property, $glue = ' ', $lang = null) + { + return join($glue, $this->all($resource, $property, 'literal', $lang)); + } + + /** Add data to the graph + * + * The resource can either be a resource or the URI of a resource. + * + * Example: + * $graph->add("http://www.example.com", 'dc:title', 'Title of Page'); + * + * @param mixed $resource The resource to add data to + * @param mixed $property The property name + * @param mixed $value The new value for the property + * + * @return integer The number of values added (1 or 0) + */ + public function add($resource, $property, $value) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + // No value given? + if ($value === null) { + return 0; + } + + // Check that the value doesn't already exist + if (isset($this->index[$resource][$property])) { + foreach ($this->index[$resource][$property] as $v) { + if ($v == $value) { + return 0; + } + } + } + $this->index[$resource][$property][] = $value; + + // Add to the reverse index if it is a resource + if ($value['type'] == 'uri' or $value['type'] == 'bnode') { + $uri = $value['value']; + $this->revIndex[$uri][$property][] = array( + 'type' => substr($resource, 0, 2) == '_:' ? 'bnode' : 'uri', + 'value' => $resource + ); + } + + // Success + return 1; + } + + /** Add a literal value as a property of a resource + * + * The resource can either be a resource or the URI of a resource. + * The value can either be a single value or an array of values. + * + * Example: + * $graph->add("http://www.example.com", 'dc:title', 'Title of Page'); + * + * @param mixed $resource The resource to add data to + * @param mixed $property The property name + * @param mixed $value The value or values for the property + * @param string $lang The language of the literal + * + * @return integer The number of values added + */ + public function addLiteral($resource, $property, $value, $lang = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + + if (is_array($value)) { + $added = 0; + foreach ($value as $v) { + $added += $this->addLiteral($resource, $property, $v, $lang); + } + return $added; + } elseif (!is_object($value) or !$value instanceof Literal) { + $value = Literal::create($value, $lang); + } + return $this->add($resource, $property, $value); + } + + /** Add a resource as a property of another resource + * + * The resource can either be a resource or the URI of a resource. + * + * Example: + * $graph->add("http://example.com/bob", 'foaf:knows', 'http://example.com/alice'); + * + * @param mixed $resource The resource to add data to + * @param mixed $property The property name + * @param mixed $resource2 The resource to be value of the property + * + * @return integer The number of values added + */ + public function addResource($resource, $property, $resource2) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkResourceParam($resource2); + + return $this->add( + $resource, + $property, + array( + 'type' => substr($resource2, 0, 2) == '_:' ? 'bnode' : 'uri', + 'value' => $resource2 + ) + ); + } + + /** Set a value for a property + * + * The new value will replace the existing values for the property. + * + * @param string $resource The resource to set the property on + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value The value for the property + * + * @return integer The number of values added (1 or 0) + */ + public function set($resource, $property, $value) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + // Delete the old values + $this->delete($resource, $property); + + // Add the new values + return $this->add($resource, $property, $value); + } + + /** Delete a property (or optionally just a specific value) + * + * @param mixed $resource The resource to delete the property from + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value The value to delete (null to delete all values) + * + * @throws \InvalidArgumentException + * @return integer The number of values deleted + */ + public function delete($resource, $property, $value = null) + { + $this->checkResourceParam($resource); + + if (is_object($property) and $property instanceof Resource) { + return $this->deleteSingleProperty($resource, $property->getUri(), $value); + } elseif (is_string($property) and preg_match('|^(\^?)<(.+)>|', $property, $matches)) { + return $this->deleteSingleProperty($resource, "$matches[1]$matches[2]", $value); + } elseif ($property === null or !is_string($property)) { + throw new \InvalidArgumentException( + '$property should be a string or EasyRdf\Resource and cannot be null' + ); + } elseif ($property === '') { + throw new \InvalidArgumentException( + "\$property cannot be an empty string" + ); + } + + // FIXME: finish implementing property paths for delete + return $this->deleteSingleProperty($resource, $property, $value); + } + + + /** Delete a property (or optionally just a specific value) + * + * @param mixed $resource The resource to delete the property from + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value The value to delete (null to delete all values) + * + * @return integer The number of values deleted + * + * @ignore + */ + public function deleteSingleProperty($resource, $property, $value = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + $count = 0; + if (isset($this->index[$resource][$property])) { + $newValues = array(); + foreach ($this->index[$resource][$property] as $k => $v) { + if (!$value or $v == $value) { + $count++; + if ($v['type'] == 'uri' or $v['type'] == 'bnode') { + $this->deleteInverse($v['value'], $property, $resource); + } + } else { + $newValues[] = $v; + } + } + + // Clean up the indexes - remove empty properties and resources + if ($count) { + if (count($newValues) == 0) { + unset($this->index[$resource][$property]); + } else { + $this->index[$resource][$property] = $newValues; + } + if (count($this->index[$resource]) == 0) { + unset($this->index[$resource]); + } + } + } + + return $count; + } + + /** Delete a resource from a property of another resource + * + * The resource can either be a resource or the URI of a resource. + * + * Example: + * $graph->delete("http://example.com/bob", 'foaf:knows', 'http://example.com/alice'); + * + * @param mixed $resource The resource to delete data from + * @param mixed $property The property name + * @param mixed $resource2 The resource value of the property to be deleted + * + * @return integer + */ + public function deleteResource($resource, $property, $resource2) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkResourceParam($resource2); + + return $this->delete( + $resource, + $property, + array( + 'type' => substr($resource2, 0, 2) == '_:' ? 'bnode' : 'uri', + 'value' => $resource2 + ) + ); + } + + /** Delete a literal value from a property of a resource + * + * Example: + * $graph->delete("http://www.example.com", 'dc:title', 'Title of Page'); + * + * @param mixed $resource The resource to add data to + * @param mixed $property The property name + * @param mixed $value The value of the property + * @param string $lang The language of the literal + * + * @return integer + */ + public function deleteLiteral($resource, $property, $value, $lang = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + if ($lang) { + $value['lang'] = $lang; + } + + return $this->delete($resource, $property, $value); + } + + /** This function is for internal use only. + * + * Deletes an inverse property from a resource. + * + * @ignore + */ + protected function deleteInverse($resource, $property, $value) + { + if (isset($this->revIndex[$resource])) { + foreach ($this->revIndex[$resource][$property] as $k => $v) { + if ($v['value'] === $value) { + unset($this->revIndex[$resource][$property][$k]); + } + } + if (count($this->revIndex[$resource][$property]) == 0) { + unset($this->revIndex[$resource][$property]); + } + if (count($this->revIndex[$resource]) == 0) { + unset($this->revIndex[$resource]); + } + } + } + + /** Check if the graph contains any statements + * + * @return boolean True if the graph contains no statements + */ + public function isEmpty() + { + return count($this->index) == 0; + } + + /** Get a list of all the shortened property names (qnames) for a resource. + * + * This method will return an empty array if the resource has no properties. + * + * @param string $resource + * + * @return array Array of shortened URIs + */ + public function properties($resource) + { + $this->checkResourceParam($resource); + + $properties = array(); + if (isset($this->index[$resource])) { + foreach ($this->index[$resource] as $property => $value) { + $short = RdfNamespace::shorten($property); + if ($short) { + $properties[] = $short; + } + } + } + return $properties; + } + + /** Get a list of the full URIs for the properties of a resource. + * + * This method will return an empty array if the resource has no properties. + * + * @param string $resource + * + * @return array Array of full URIs + */ + public function propertyUris($resource) + { + $this->checkResourceParam($resource); + + if (isset($this->index[$resource])) { + return array_keys($this->index[$resource]); + } else { + return array(); + } + } + + /** Get a list of the full URIs for the properties that point to a resource. + * + * @param string $resource + * + * @return array Array of full property URIs + */ + public function reversePropertyUris($resource) + { + $this->checkResourceParam($resource); + + if (isset($this->revIndex[$resource])) { + return array_keys($this->revIndex[$resource]); + } else { + return array(); + } + } + + /** Check to see if a property exists for a resource. + * + * This method will return true if the property exists. + * If the value parameter is given, then it will only return true + * if the value also exists for that property. + * + * By providing a value parameter you can use this function to check + * to see if a triple exists in the graph. + * + * @param mixed $resource The resource to check + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value An optional value of the property + * + * @return boolean True if value the property exists. + */ + public function hasProperty($resource, $property, $value = null) + { + $this->checkResourceParam($resource); + $this->checkSinglePropertyParam($property, $inverse); + $this->checkValueParam($value); + + // Use the reverse index if it is an inverse property + if ($inverse) { + $index = &$this->revIndex; + } else { + $index = &$this->index; + } + + if (isset($index[$resource][$property])) { + if (is_null($value)) { + return true; + } else { + foreach ($index[$resource][$property] as $v) { + if ($v == $value) { + return true; + } + } + } + } + + return false; + } + + /** Serialise the graph into RDF + * + * The $format parameter can be an EasyRdf\Format object, a + * format name, a mime type or a file extension. + * + * Example: + * $turtle = $graph->serialise('turtle'); + * + * @param mixed $format The format to serialise to + * @param array $options Serialiser-specific options, for fine-tuning the output + * + * @return mixed The serialised graph + */ + public function serialise($format, array $options = array()) + { + if (!$format instanceof Format) { + $format = Format::getFormat($format); + } + $serialiser = $format->newSerialiser(); + return $serialiser->serialise($this, $format->getName(), $options); + } + + /** Return a human readable view of all the resources in the graph + * + * This method is intended to be a debugging aid and will + * return a pretty-print view of all the resources and their + * properties. + * + * @param string $format Either 'html' or 'text' + * + * @return string + */ + public function dump($format = 'html') + { + $result = ''; + if ($format == 'html') { + $result .= "
". + "Graph: ". $this->uri . "
\n"; + } else { + $result .= "Graph: ". $this->uri . "\n"; + } + + foreach ($this->index as $resource => $properties) { + $result .= $this->dumpResource($resource, $format); + } + return $result; + } + + /** Return a human readable view of a resource and its properties + * + * This method is intended to be a debugging aid and will + * print a resource and its properties. + * + * @param mixed $resource The resource to dump + * @param string $format Either 'html' or 'text' + * + * @return string + */ + public function dumpResource($resource, $format = 'html') + { + $this->checkResourceParam($resource, true); + + if (isset($this->index[$resource])) { + $properties = $this->index[$resource]; + } else { + return ''; + } + + $plist = array(); + foreach ($properties as $property => $values) { + $olist = array(); + foreach ($values as $value) { + if ($value['type'] == 'literal') { + $olist []= Utils::dumpLiteralValue($value, $format, 'black'); + } else { + $olist []= Utils::dumpResourceValue($value['value'], $format, 'blue'); + } + } + + $pstr = RdfNamespace::shorten($property); + if ($pstr == null) { + $pstr = $property; + } + if ($format == 'html') { + $plist []= " ". + "". + htmlentities($pstr) . " ". + " ". + join(", ", $olist); + } else { + $plist []= " -> $pstr -> " . join(", ", $olist); + } + } + + if ($format == 'html') { + return "
\n". + "
".Utils::dumpResourceValue($resource, $format, 'blue')." ". + "(". + $this->classForResource($resource).")
\n". + "
\n". + "
".join("
\n
", $plist)."
". + "
\n"; + } else { + return $resource." (".$this->classForResource($resource).")\n" . + join("\n", $plist) . "\n\n"; + } + } + + /** Get the resource type of the graph + * + * The type will be a shortened URI as a string. + * If the graph has multiple types then the type returned + * may be arbitrary. + * This method will return null if the resource has no type. + * + * @param string|null $resource + * + * @return string A type assocated with the resource (e.g. foaf:Document) + */ + public function type($resource = null) + { + $type = $this->typeAsResource($resource); + + if ($type) { + return RdfNamespace::shorten($type); + } + + return null; + } + + /** Get the resource type of the graph as a EasyRdf\Resource + * + * If the graph has multiple types then the type returned + * may be arbitrary. + * This method will return null if the resource has no type. + * + * @param mixed $resource + * + * @return \EasyRdf\Resource A type associated with the resource + */ + public function typeAsResource($resource = null) + { + $this->checkResourceParam($resource, true); + + if ($resource) { + return $this->get($resource, 'rdf:type', 'resource'); + } + + return null; + } + + /** Get a list of types for a resource + * + * The types will each be a shortened URI as a string. + * This method will return an empty array if the resource has no types. + * + * If $resource is null, then it will get the types for the URI of the graph. + * + * @param string|null $resource + * + * @return array All types assocated with the resource (e.g. foaf:Person) + */ + public function types($resource = null) + { + $resources = $this->typesAsResources($resource); + + $types = array(); + foreach ($resources as $type) { + $types[] = RdfNamespace::shorten($type); + } + + return $types; + } + + /** + * Get the resource types of the graph as a EasyRdf\Resource + * + * @param string|null $resource + * + * @return Resource[] + */ + public function typesAsResources($resource = null) + { + $this->checkResourceParam($resource, true); + + if ($resource) { + return $this->all($resource, 'rdf:type', 'resource'); + } + + return array(); + } + + /** Check if a resource is of the specified type + * + * @param string $resource The resource to check the type of + * @param string $type The type to check (e.g. foaf:Person) + * + * @return boolean True if resource is of specified type + */ + public function isA($resource, $type) + { + $this->checkResourceParam($resource, true); + $this->checkResourceParam($type, true); + + foreach ($this->all($resource, 'rdf:type', 'resource') as $t) { + if ($t->getUri() == $type) { + return true; + } + } + return false; + } + + /** Add one or more rdf:type properties to a resource + * + * @param string $resource The resource to add the type to + * @param string $types One or more types to add (e.g. foaf:Person) + * + * @return integer The number of types added + */ + public function addType($resource, $types) + { + $this->checkResourceParam($resource, true); + + if (!is_array($types)) { + $types = array($types); + } + + $count = 0; + foreach ($types as $type) { + $type = RdfNamespace::expand($type); + $count += $this->add($resource, 'rdf:type', array('type' => 'uri', 'value' => $type)); + } + + return $count; + } + + /** Change the rdf:type property for a resource + * + * Note that if the resource object has already previously + * been created, then the PHP class of the resource will not change. + * + * @param string $resource The resource to change the type of + * @param string $type The new type (e.g. foaf:Person) + * + * @return integer The number of types added + */ + public function setType($resource, $type) + { + $this->checkResourceParam($resource, true); + + $this->delete($resource, 'rdf:type'); + return $this->addType($resource, $type); + } + + /** Get a human readable label for a resource + * + * This method will check a number of properties for a resource + * (in the order: skos:prefLabel, rdfs:label, foaf:name, dc:title) + * and return an approriate first that is available. If no label + * is available then it will return null. + * + * @param string|null $resource + * @param string|null $lang + * + * @return string A label for the resource. + */ + public function label($resource = null, $lang = null) + { + $this->checkResourceParam($resource, true); + + if ($resource) { + return $this->get( + $resource, + 'skos:prefLabel|rdfs:label|foaf:name|rss:title|dc:title|dc11:title', + 'literal', + $lang + ); + } else { + return null; + } + } + + /** Get the primary topic of the graph + * + * @param mixed $resource + * + * @return \EasyRdf\Resource The primary topic of the document. + */ + public function primaryTopic($resource = null) + { + $this->checkResourceParam($resource, true); + + if ($resource) { + return $this->get( + $resource, + 'foaf:primaryTopic|^foaf:isPrimaryTopicOf', + 'resource' + ); + } else { + return null; + } + } + + /** Returns the graph as a RDF/PHP associative array + * + * @return array The contents of the graph as an array. + */ + public function toRdfPhp() + { + return $this->index; + } + + /** Calculates the number of triples in the graph + * + * @return integer The number of triples in the graph. + */ + public function countTriples() + { + $count = 0; + foreach ($this->index as $resource) { + foreach ($resource as $values) { + $count += count($values); + } + } + return $count; + } + + /** Magic method to return URI of resource when casted to string + * + * @return string The URI of the resource + */ + public function __toString() + { + return $this->uri == null ? '' : $this->uri; + } + + /** Magic method to get a property of the graph + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * $value = $graph->title; + * + * @see RdfNamespace::setDefault() + * @param string $name The name of the property + * + * @return string A single value for the named property + */ + public function __get($name) + { + return $this->get($this->uri, $name); + } + + /** Magic method to set the value for a property of the graph + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * $graph->title = 'Title'; + * + * @see RdfNamespace::setDefault() + * @param string $name The name of the property + * @param string $value The value for the property + * + * @return integer + */ + public function __set($name, $value) + { + return $this->set($this->uri, $name, $value); + } + + /** Magic method to check if a property exists + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * if (isset($graph->title)) { blah(); } + * + * @see RdfNamespace::setDefault() + * @param string $name The name of the property + * + * @return boolean + */ + public function __isset($name) + { + return $this->hasProperty($this->uri, $name); + } + + /** Magic method to delete a property of the graph + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * unset($graph->title); + * + * @see RdfNamespace::setDefault() + * @param string $name The name of the property + * + * @return integer + */ + public function __unset($name) + { + return $this->delete($this->uri, $name); + } +} diff --git a/lib/GraphStore.php b/lib/GraphStore.php new file mode 100644 index 0000000..5538958 --- /dev/null +++ b/lib/GraphStore.php @@ -0,0 +1,312 @@ +uri = $uri; + $this->parsedUri = new ParsedUri($uri); + } + + /** Get the URI of the graph store + * + * @return string The URI of the graph store + */ + public function getUri() + { + return $this->uri; + } + + /** Fetch a named graph from the graph store + * + * The URI can either be a full absolute URI or + * a URI relative to the URI of the graph store. + * + * @param string $uriRef The URI of graph desired + * + * @return Graph The graph requested + */ + public function get($uriRef) + { + if ($uriRef === self::DEFAULT_GRAPH) { + $dataUrl = $this->urlForGraph(self::DEFAULT_GRAPH); + $graph = new Graph(); + } else { + $graphUri = $this->parsedUri->resolve($uriRef)->toString(); + $dataUrl = $this->urlForGraph($graphUri); + + $graph = new Graph($graphUri); + } + + $graph->load($dataUrl); + + return $graph; + } + + /** + * Fetch default graph from the graph store + * @return Graph + */ + public function getDefault() + { + return $this->get(self::DEFAULT_GRAPH); + } + + /** Send some graph data to the graph store + * + * This method is used by insert() and replace() + * + * @ignore + */ + protected function sendGraph($method, $graph, $uriRef, $format) + { + if (is_object($graph) and $graph instanceof Graph) { + if ($uriRef === null) { + $uriRef = $graph->getUri(); + } + $data = $graph->serialise($format); + } else { + $data = $graph; + } + + if ($uriRef === null) { + throw new \InvalidArgumentException('Graph IRI is not specified'); + } + + $formatObj = Format::getFormat($format); + $mimeType = $formatObj->getDefaultMimeType(); + + if ($uriRef === self::DEFAULT_GRAPH) { + $dataUrl = $this->urlForGraph(self::DEFAULT_GRAPH); + } else { + $graphUri = $this->parsedUri->resolve($uriRef)->toString(); + $dataUrl = $this->urlForGraph($graphUri); + } + + $client = Http::getDefaultHttpClient(); + $client->resetParameters(true); + $client->setUri($dataUrl); + $client->setMethod($method); + $client->setRawData($data); + $client->setHeaders('Content-Type', $mimeType); + + $response = $client->request(); + + if (!$response->isSuccessful()) { + throw new Exception( + "HTTP request for {$dataUrl} failed: ".$response->getMessage() + ); + } + + return $response; + } + + /** Replace the contents of a graph in the graph store with new data + * + * The $graph parameter is the EasyRdf\Graph object to be sent to the + * graph store. Alternatively it can be a string, already serialised. + * + * The URI can either be a full absolute URI or + * a URI relative to the URI of the graph store. + * + * The $format parameter can be given to specify the serialisation + * used to send the graph data to the graph store. + * + * @param Graph|string $graph Data + * @param string $uriRef The URI of graph to be replaced + * @param string $format The format of the data to send to the graph store + * + * @return Http\Response The response from the graph store + */ + public function replace($graph, $uriRef = null, $format = 'ntriples') + { + return $this->sendGraph('PUT', $graph, $uriRef, $format); + } + + /** + * Replace the contents of default graph in the graph store with new data + * + * The $graph parameter is the EasyRdf\Graph object to be sent to the + * graph store. Alternatively it can be a string, already serialised. + * + * The $format parameter can be given to specify the serialisation + * used to send the graph data to the graph store. + * + * @param Graph|string $graph Data + * @param string $format The format of the data to send to the graph store + * + * @return Http\Response The response from the graph store + */ + public function replaceDefault($graph, $format = 'ntriples') + { + return self::replace($graph, self::DEFAULT_GRAPH, $format); + } + + /** Add data to a graph in the graph store + * + * The $graph parameter is the EasyRdf\Graph object to be sent to the + * graph store. Alternatively it can be a string, already serialised. + * + * The URI can either be a full absolute URI or + * a URI relative to the URI of the graph store. + * + * The $format parameter can be given to specify the serialisation + * used to send the graph data to the graph store. + * + * @param Graph|string $graph Data + * @param string $uriRef The URI of graph to be added to + * @param string $format The format of the data to send to the graph store + * + * @return Http\Response The response from the graph store + */ + public function insert($graph, $uriRef = null, $format = 'ntriples') + { + return $this->sendGraph('POST', $graph, $uriRef, $format); + } + + /** + * Add data to default graph of the graph store + * + * The $graph parameter is the EasyRdf\Graph object to be sent to the + * graph store. Alternatively it can be a string, already serialised. + * + * The $format parameter can be given to specify the serialisation + * used to send the graph data to the graph store. + * + * @param Graph|string $graph Data + * @param string $format The format of the data to send to the graph store + * + * @return Http\Response The response from the graph store + */ + public function insertIntoDefault($graph, $format = 'ntriples') + { + return $this->insert($graph, self::DEFAULT_GRAPH, $format); + } + + /** Delete named graph content from the graph store + * + * The URI can either be a full absolute URI or + * a URI relative to the URI of the graph store. + * + * @param string $uriRef The URI of graph to be added to + * + * @throws Exception + * @return Http\Response The response from the graph store + */ + public function delete($uriRef) + { + if ($uriRef === self::DEFAULT_GRAPH) { + $dataUrl = $this->urlForGraph(self::DEFAULT_GRAPH); + } else { + $graphUri = $this->parsedUri->resolve($uriRef)->toString(); + $dataUrl = $this->urlForGraph($graphUri); + } + + $client = Http::getDefaultHttpClient(); + $client->resetParameters(true); + $client->setUri($dataUrl); + $client->setMethod('DELETE'); + $response = $client->request(); + + if (!$response->isSuccessful()) { + throw new Exception( + "HTTP request to delete {$dataUrl} failed: ".$response->getMessage() + ); + } + + return $response; + } + + /** + * Delete default graph content from the graph store + * + * @return Http\Response + * @throws Exception + */ + public function deleteDefault() + { + return $this->delete(self::DEFAULT_GRAPH); + } + + /** Work out the full URL for a graph store request. + * by checking if if it is a direct or indirect request. + * @ignore + */ + protected function urlForGraph($url) + { + if ($url === self::DEFAULT_GRAPH) { + $url = $this->uri.'?default'; + } elseif (strpos($url, $this->uri) === false) { + $url = $this->uri."?graph=".urlencode($url); + } + + return $url; + } + + /** Magic method to return URI of the graph store when casted to string + * + * @return string The URI of the graph store + */ + public function __toString() + { + return empty($this->uri) ? '' : $this->uri; + } +} diff --git a/lib/Http.php b/lib/Http.php new file mode 100644 index 0000000..c705af9 --- /dev/null +++ b/lib/Http.php @@ -0,0 +1,85 @@ + 5, + 'useragent' => 'EasyRdf HTTP Client', + 'timeout' => 10 + ); + + /** + * Request URI + * + * @var string + */ + private $uri = null; + + /** + * Associative array of request headers + * + * @var array + */ + private $headers = array(); + + /** + * HTTP request method + * + * @var string + */ + private $method = 'GET'; + + /** + * Associative array of GET parameters + * + * @var array + */ + private $paramsGet = array(); + + /** + * The raw post data to send. Could be set by setRawData($data). + * + * @var string + */ + private $rawPostData = null; + + /** + * Redirection counter + * + * @var int + */ + private $redirectCounter = 0; + + /** + * Constructor method. Will create a new HTTP client. Accepts the target + * URL and optionally configuration array. + * + * @param string $uri + * @param array $config Configuration key-value pairs. + */ + public function __construct($uri = null, $config = null) + { + if ($uri !== null) { + $this->setUri($uri); + } + if ($config !== null) { + $this->setConfig($config); + } + } + + /** + * Set the URI for the next request + * + * @param string $uri + * + * @throws \InvalidArgumentException + * @return self + */ + public function setUri($uri) + { + if (!is_string($uri)) { + $uri = strval($uri); + } + + if (!preg_match('/^http(s?):/', $uri)) { + throw new \InvalidArgumentException( + "EasyRdf\\Http\\Client only supports the 'http' and 'https' schemes." + ); + } + + $this->uri = $uri; + + return $this; + } + + /** + * Get the URI for the next request + * + * @param bool $asString + * + * @return string + */ + public function getUri($asString = true) + { + return $this->uri; + } + + /** + * Set configuration parameters for this HTTP client + * + * @param array $config + * + * @return self + * @throws \InvalidArgumentException + */ + public function setConfig($config = array()) + { + if ($config == null or !is_array($config)) { + throw new \InvalidArgumentException( + "\$config should be an array and cannot be null" + ); + } + + foreach ($config as $k => $v) { + $this->config[strtolower($k)] = $v; + } + + return $this; + } + + /** + * Set a request header + * + * @param string $name Header name (e.g. 'Accept') + * @param string $value Header value or null + * + * @return self + */ + public function setHeaders($name, $value = null) + { + $normalizedName = strtolower($name); + + // If $value is null or false, unset the header + if ($value === null || $value === false) { + unset($this->headers[$normalizedName]); + } else { + // Else, set the header + $this->headers[$normalizedName] = array($name, $value); + } + + return $this; + } + + /** + * Set the next request's method + * + * Validated the passed method and sets it. + * + * @param string $method + * + * @return self + * @throws \InvalidArgumentException + */ + public function setMethod($method) + { + if (!is_string($method) or !preg_match('/^[A-Z]+$/', $method)) { + throw new \InvalidArgumentException("Invalid HTTP request method."); + } + + $this->method = $method; + + return $this; + } + + /** + * Get the method for the next request + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Get the value of a specific header + * + * Note that if the header has more than one value, an array + * will be returned. + * + * @param string $key + * + * @return string|array|null The header value or null if it is not set + */ + public function getHeader($key) + { + $key = strtolower($key); + if (isset($this->headers[$key])) { + return $this->headers[$key][1]; + } else { + return null; + } + } + + /** + * Set a GET parameter for the request. + * + * @param string $name + * @param string $value + * + * @return self + */ + public function setParameterGet($name, $value = null) + { + if ($value === null) { + if (isset($this->paramsGet[$name])) { + unset($this->paramsGet[$name]); + } + } else { + $this->paramsGet[$name] = $value; + } + + return $this; + } + + /** + * Get a GET parameter for the request. + * + * @param string $name + * + * @return string value + */ + public function getParameterGet($name) + { + if (isset($this->paramsGet[$name])) { + return $this->paramsGet[$name]; + } else { + return null; + } + } + + /** + * Get all the GET parameters + * + * @return array + */ + public function getParametersGet() + { + return $this->paramsGet; + } + + /** + * Get the number of redirections done on the last request + * + * @return int + */ + public function getRedirectionsCount() + { + return $this->redirectCounter; + } + + /** + * Set the raw (already encoded) POST data. + * + * This function is here for two reasons: + * 1. For advanced user who would like to set their own data, already encoded + * 2. For backwards compatibilty: If someone uses the old post($data) method. + * this method will be used to set the encoded data. + * + * $data can also be stream (such as file) from which the data will be read. + * + * @param string|resource $data + * + * @return self + */ + public function setRawData($data) + { + $this->rawPostData = $data; + return $this; + } + + /** + * Get the raw (already encoded) POST data. + * + * @return string + */ + public function getRawData() + { + return $this->rawPostData; + } + + /** + * Clear all GET and POST parameters + * + * Should be used to reset the request parameters if the client is + * used for several concurrent requests. + * + * clearAll parameter controls if we clean just parameters or also + * headers + * + * @param bool $clearAll Should all data be cleared? + * + * @return self + */ + public function resetParameters($clearAll = false) + { + // Reset parameter data + $this->paramsGet = array(); + $this->rawPostData = null; + $this->method = 'GET'; + + if ($clearAll) { + $this->headers = array(); + } else { + // Clear outdated headers + if (isset($this->headers['content-type'])) { + unset($this->headers['content-type']); + } + if (isset($this->headers['content-length'])) { + unset($this->headers['content-length']); + } + } + + return $this; + } + + /** + * Send the HTTP request and return an HTTP response object + * + * @param null|string $method + * + * @throws \EasyRdf\Exception + * @return Response + */ + public function request($method = null) + { + if (!$this->uri) { + throw new Exception( + "Set URI before calling Client->request()" + ); + } + + if ($method) { + $this->setMethod($method); + } + $this->redirectCounter = 0; + $response = null; + + // Send the first request. If redirected, continue. + do { + // Clone the URI and add the additional GET parameters to it + $uri = parse_url($this->uri); + if ($uri['scheme'] === 'http') { + $host = $uri['host']; + } elseif ($uri['scheme'] === 'https') { + $host = 'ssl://'.$uri['host']; + } else { + throw new Exception( + "Unsupported URI scheme: ".$uri['scheme'] + ); + } + + if (isset($uri['port'])) { + $port = $uri['port']; + } else { + if ($uri['scheme'] === 'https') { + $port = 443; + } else { + $port = 80; + } + } + + if (!empty($this->paramsGet)) { + if (!empty($uri['query'])) { + $uri['query'] .= '&'; + } else { + $uri['query'] = ''; + } + $uri['query'] .= http_build_query($this->paramsGet, null, '&'); + } + + $headers = $this->prepareHeaders($uri['host'], $port); + + // Open socket to remote server + $socket = @fsockopen($host, $port, $errno, $errstr, $this->config['timeout']); + if (!$socket) { + throw new Exception("Unable to connect to $host:$port ($errstr)"); + } + stream_set_timeout($socket, $this->config['timeout']); + $info = stream_get_meta_data($socket); + + // Write the request + $path = $uri['path']; + if (empty($path)) { + $path = '/'; + } + if (isset($uri['query'])) { + $path .= '?' . $uri['query']; + } + fwrite($socket, "{$this->method} {$path} HTTP/1.1\r\n"); + foreach ($headers as $k => $v) { + if (is_string($k)) { + $v = ucfirst($k) . ": $v"; + } + fwrite($socket, "$v\r\n"); + } + fwrite($socket, "\r\n"); + + // Send the request body, if there is one set + if (isset($this->rawPostData)) { + fwrite($socket, $this->rawPostData); + } + + // Read in the response + $content = ''; + while (!feof($socket) && !$info['timed_out']) { + $content .= fgets($socket); + $info = stream_get_meta_data($socket); + } + + if ($info['timed_out']) { + throw new Exception("Request to $host:$port timed out"); + } + + // FIXME: support HTTP/1.1 100 Continue + + // Close the socket + @fclose($socket); + + // Parse the response string + $response = Response::fromString($content); + + // If we got redirected, look for the Location header + if ($response->isRedirect() && + ($location = $response->getHeader('location')) + ) { + // Avoid problems with buggy servers that add whitespace at the + // end of some headers (See ZF-11283) + $location = trim($location); + + // Some servers return relative URLs in the location header + // resolve it in relation to previous request + $baseUri = new ParsedUri($this->uri); + $location = $baseUri->resolve($location)->toString(); + + // If it is a 303 then drop the parameters and send a GET request + if ($response->getStatus() == 303) { + $this->resetParameters(); + $this->setMethod('GET'); + } + + // If we got a well formed absolute URI + if (parse_url($location)) { + $this->setHeaders('host', null); + $this->setUri($location); + } else { + throw new Exception( + "Failed to parse Location header returned by ". + $this->uri + ); + } + ++$this->redirectCounter; + } else { + // If we didn't get any location, stop redirecting + break; + } + } while ($this->redirectCounter < $this->config['maxredirects']); + + return $response; + } + + /** + * Prepare the request headers + * + * @ignore + * + * @param $host + * @param $port + * + * @return array + */ + protected function prepareHeaders($host, $port) + { + $headers = array(); + + // Set the host header + if (! isset($this->headers['host'])) { + // If the port is not default, add it + if ($port !== 80 and $port !== 443) { + $host .= ':' . $port; + } + $headers[] = "Host: {$host}"; + } + + // Set the connection header + if (! isset($this->headers['connection'])) { + $headers[] = "Connection: close"; + } + + // Set the user agent header + if (! isset($this->headers['user-agent'])) { + $headers[] = "User-Agent: {$this->config['useragent']}"; + } + + // If we have rawPostData set, set the content-length header + if (isset($this->rawPostData)) { + $headers[] = "Content-Length: ".strlen($this->rawPostData); + } + + // Add all other user defined headers + foreach ($this->headers as $header) { + list($name, $value) = $header; + if (is_array($value)) { + $value = implode(', ', $value); + } + + $headers[] = "$name: $value"; + } + + return $headers; + } +} diff --git a/lib/Http/Exception.php b/lib/Http/Exception.php new file mode 100644 index 0000000..b11a7b1 --- /dev/null +++ b/lib/Http/Exception.php @@ -0,0 +1,18 @@ +body = $body; + } + + public function getBody() + { + return $this->body; + } +} diff --git a/lib/Http/Response.php b/lib/Http/Response.php new file mode 100644 index 0000000..728955d --- /dev/null +++ b/lib/Http/Response.php @@ -0,0 +1,431 @@ +status = (int) $status; + $this->body = $body; + $this->version = $version; + $this->message = $message; + + foreach ($headers as $k => $v) { + $k = ucwords(strtolower($k)); + $this->headers[$k] = $v; + } + } + + /** + * Check whether the response in successful + * + * @return boolean + */ + public function isSuccessful() + { + return ($this->status >= 200 && $this->status < 300); + } + + /** + * Check whether the response is an error + * + * @return boolean + */ + public function isError() + { + return ($this->status >= 400 && $this->status < 600); + } + + /** + * Check whether the response is a redirection + * + * @return boolean + */ + public function isRedirect() + { + return ($this->status >= 300 && $this->status < 400); + } + + /** + * Get the HTTP response status code + * + * @return int + */ + public function getStatus() + { + return $this->status; + } + + /** + * Return a message describing the HTTP response code + * (Eg. "OK", "Not Found", "Moved Permanently") + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Get the response body as string + * + * @return string + */ + public function getBody() + { + $body = $this->body; + + if ('chunked' === strtolower($this->getHeader('transfer-encoding'))) { + $body = self::decodeChunkedBody($body); + } + + $contentEncoding = strtolower($this->getHeader('content-encoding')); + + if ('gzip' === $contentEncoding) { + $body = self::decodeGzip($body); + } elseif ('deflate' === $contentEncoding) { + $body = self::decodeDeflate($body); + } + + return $body; + } + + /** + * Get the raw response body (as transfered "on wire") as string + * + * If the body is encoded (with Transfer-Encoding, not content-encoding - + * IE "chunked" body), gzip compressed, etc. it will not be decoded. + * + * @return string + */ + public function getRawBody() + { + return $this->body; + } + + /** + * Get the HTTP version of the response + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Get the response headers + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Get a specific header as string, or null if it is not set + * + * @param string $header + * + * @return string|array|null + */ + public function getHeader($header) + { + $header = ucwords(strtolower($header)); + if (array_key_exists($header, $this->headers)) { + return $this->headers[$header]; + } + + return null; + } + + /** + * Get all headers as string + * + * @param boolean $statusLine Whether to return the first status line (ie "HTTP 200 OK") + * @param string $br Line breaks (eg. "\n", "\r\n", "
") + * + * @return string + */ + public function getHeadersAsString($statusLine = true, $br = "\n") + { + $str = ''; + + if ($statusLine) { + $str = "HTTP/{$this->version} {$this->status} {$this->message}{$br}"; + } + + // Iterate over the headers and stringify them + foreach ($this->headers as $name => $value) { + if (is_string($value)) { + $str .= "{$name}: {$value}{$br}"; + } elseif (is_array($value)) { + foreach ($value as $subval) { + $str .= "{$name}: {$subval}{$br}"; + } + } + } + + return $str; + } + + /** + * Create an EasyRdf\Http\Response object from a HTTP response string + * + * @param string $responseStr + * + * @throws \EasyRdf\Exception + * @return self + */ + public static function fromString($responseStr) + { + // First, split body and headers + $matches = preg_split('|(?:\r?\n){2}|m', $responseStr, 2); + if ($matches and 2 === count($matches)) { + list ($headerLines, $body) = $matches; + } else { + throw new Exception( + "Failed to parse HTTP response." + ); + } + + // Split headers part to lines + $headerLines = preg_split('|[\r\n]+|m', $headerLines); + $status = array_shift($headerLines); + if (preg_match("|^HTTP\/([\d\.x]+) (\d+) ?([^\r\n]*)|", $status, $m)) { + $version = $m[1]; + $status = $m[2]; + $message = $m[3] ? $m[3] : null; + } else { + throw new Exception( + "Failed to parse HTTP response status line." + ); + } + + // Process the rest of the header lines + $headers = array(); + foreach ($headerLines as $line) { + if (preg_match("|^([\w-]+):\s+(.+)$|", $line, $m)) { + $hName = ucwords(strtolower($m[1])); + $hValue = $m[2]; + + if (isset($headers[$hName])) { + if (! is_array($headers[$hName])) { + $headers[$hName] = array($headers[$hName]); + } + $headers[$hName][] = $hValue; + } else { + $headers[$hName] = $hValue; + } + } + } + + return new self($status, $headers, $body, $version, $message); + } + + + /** + * Decode a "chunked" transfer-encoded body and return the decoded text + * + * @param string $body + * + * @throws \EasyRdf\Exception + * + * @return string + */ + public static function decodeChunkedBody($body) + { + $decBody = ''; + + while (trim($body)) { + if (!preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) { + throw new Exception( + "Error parsing body - doesn't seem to be a chunked message" + ); + } + $length = hexdec(trim($m[1])); + $cut = strlen($m[0]); + $decBody .= substr($body, $cut, $length); + $body = substr($body, $cut + $length + 2); + } + + return $decBody; + } + + /** + * Decode a gzip encoded message (when Content-encoding = gzip) + * + * Currently requires PHP with zlib support + * + * @param string $body + * + * @throws Exception + * + * @return string + */ + public static function decodeGzip($body) + { + if (!function_exists('gzinflate')) { + throw new Exception( + 'zlib extension is required in order to decode "gzip" encoding' + ); + } + + return gzinflate(substr($body, 10)); + } + + /** + * Decode a zlib deflated message (when Content-encoding = deflate) + * + * Currently requires PHP with zlib support + * + * @param string $body + * + * @throws Exception + * + * @return string + */ + public static function decodeDeflate($body) + { + if (!function_exists('gzuncompress')) { + throw new Exception( + 'zlib extension is required in order to decode "deflate" encoding' + ); + } + + /** + * Some servers (IIS ?) send a broken deflate response, without the + * RFC-required zlib header. + * + * We try to detect the zlib header, and if it does not exist we + * teat the body is plain DEFLATE content. + * + * This method was adapted from PEAR HTTP_Request2 by (c) Alexey Borzov + * + * @link http://framework.zend.com/issues/browse/ZF-6040 + */ + $zlibHeader = unpack('n', substr($body, 0, 2)); + + if ($zlibHeader[1] % 31 === 0) { + return gzuncompress($body); + } + + return gzinflate($body); + } + + + /** + * Get the entire response as string + * + * @param string $br Line breaks (eg. "\n", "\r\n", "
") + * + * @return string + */ + public function asString($br = "\n") + { + return $this->getHeadersAsString(true, $br) . $br . $this->getRawBody(); + } + + /** + * Implements magic __toString() + * + * @return string + */ + public function __toString() + { + return $this->asString(); + } +} diff --git a/lib/Isomorphic.php b/lib/Isomorphic.php new file mode 100644 index 0000000..a84a06d --- /dev/null +++ b/lib/Isomorphic.php @@ -0,0 +1,437 @@ + 0 or count($bnodesB) > 0) { + // There are blank nodes - build a bi-jection + return self::buildBijectionTo($statementsA, $bnodesA, $statementsB, $bnodesB); + } else { + // No bnodes and the grounded statements match + return array(); + } + } + + /** + * Count the number of subjects in a graph + * @ignore + */ + private static function countSubjects($graph) + { + return count($graph->toRdfPhp()); + } + + /** + * Check if all the statements in $graphA also appear in $graphB + * @ignore + */ + private static function groundedStatementsMatch($graphA, $graphB, &$bnodes, &$anonStatements) + { + $groundedStatementsMatch = true; + + foreach ($graphA->toRdfPhp() as $subject => $properties) { + if (substr($subject, 0, 2) == '_:') { + array_push($bnodes, $subject); + $subjectIsBnode = true; + } else { + $subjectIsBnode = false; + } + + foreach ($properties as $property => $values) { + foreach ($values as $value) { + if ($value['type'] == 'uri' and substr($value['value'], 0, 2) == '_:') { + array_push($bnodes, $value['value']); + $objectIsBnode = true; + } else { + $objectIsBnode = false; + } + + if ($groundedStatementsMatch and + $subjectIsBnode === false and + $objectIsBnode === false and + $graphB->hasProperty($subject, $property, $value) === false + ) { + $groundedStatementsMatch = false; + } + + if ($subjectIsBnode or $objectIsBnode) { + array_push( + $anonStatements, + array( + array('type' => $subjectIsBnode ? 'bnode' : 'uri', 'value' => $subject), + array('type' => 'uri', 'value' => $property), + $value + ) + ); + } + } + } + } + + return $groundedStatementsMatch; + } + + /** + * The main recursive bijection algorithm. + * + * This algorithm is very similar to the one explained by Jeremy Carroll in + * http://www.hpl.hp.com/techreports/2001/HPL-2001-293.pdf. Page 12 has the + * relevant pseudocode. + * + * @ignore + */ + private static function buildBijectionTo( + $statementsA, + $nodesA, + $statementsB, + $nodesB, + $groundedHashesA = array(), + $groundedHashesB = array() + ) { + + // Create a hash signature of every node, based on the signature of + // statements it exists in. + // We also save hashes of nodes that cannot be reliably known; we will use + // that information to eliminate possible recursion combinations. + // + // Any mappings given in the method parameters are considered grounded. + list($hashesA, $ungroundedHashesA) = self::hashNodes($statementsA, $nodesA, $groundedHashesA); + list($hashesB, $ungroundedHashesB) = self::hashNodes($statementsB, $nodesB, $groundedHashesB); + + // Grounded hashes are built at the same rate between the two graphs (if + // they are isomorphic). If there exists a grounded node in one that is + // not in the other, we can just return. Ungrounded nodes might still + // conflict, so we don't check them. This is a little bit messy in the + // middle of the method, and probably slows down isomorphic checks, but + // prevents almost-isomorphic cases from getting nutty. + foreach ($hashesA as $hashA) { + if (!in_array($hashA, $hashesB)) { + return null; + } + } + foreach ($hashesB as $hashB) { + if (!in_array($hashB, $hashesA)) { + return null; + } + } + + // Using the created hashes, map nodes to other_nodes + // Ungrounded hashes will also be equal, but we keep the distinction + // around for when we recurse later (we only recurse on ungrounded nodes) + $bijection = array(); + foreach ($nodesA as $nodeA) { + $foundNode = null; + foreach ($ungroundedHashesB as $nodeB => $hashB) { + if ($ungroundedHashesA[$nodeA] == $hashB) { + $foundNode = $nodeB; + } + } + + if ($foundNode) { + $bijection[$nodeA] = $foundNode; + + // Deletion is required to keep counts even; two nodes with identical + // signatures can biject to each other at random. + unset($ungroundedHashesB[$foundNode]); + } + } + + // bijection is now a mapping of nodes to other_nodes. If all are + // accounted for on both sides, we have a bijection. + // + // If not, we will speculatively mark pairs with matching ungrounded + // hashes as bijected and recurse. + $bijectionA = array_keys($bijection); + $bijectionB = array_values($bijection); + sort($bijectionA); + sort($nodesA); + sort($bijectionB); + sort($nodesB); + if ($bijectionA != $nodesA or $bijectionB != $nodesB) { + $bijection = null; + + foreach ($nodesA as $nodeA) { + // We don't replace grounded nodes' hashes + if (isset($hashesA[$nodeA])) { + continue; + } + + foreach ($nodesB as $nodeB) { + // We don't replace grounded nodesB's hashes + if (isset($hashesB[$nodeB])) { + continue; + } + + // The ungrounded signature must match for this to potentially work + if ($ungroundedHashesA[$nodeA] != $ungroundedHashesB[$nodeB]) { + continue; + } + + $hash = sha1($nodeA); + $hashesA[$nodeA] = $hash; + $hashesB[$nodeB] = $hash; + $bijection = self::buildBijectionTo( + $statementsA, + $nodesA, + $statementsB, + $nodesA, + $hashesA, + $hashesB + ); + } + } + } + + return $bijection; + } + + /** + * Given a set of statements, create a mapping of node => SHA1 for a given + * set of blank nodes. grounded_hashes is a mapping of node => SHA1 pairs + * that we will take as a given, and use those to make more specific + * signatures of other nodes. + * + * Returns a tuple of associative arrats: one of grounded hashes, and one of all + * hashes. grounded hashes are based on non-blank nodes and grounded blank + * nodes, and can be used to determine if a node's signature matches + * another. + * + * @ignore + */ + private static function hashNodes($statements, $nodes, $groundedHahes) + { + $hashes = $groundedHahes; + $ungroundedHashes = array(); + $hashNeeded = true; + + // We may have to go over the list multiple times. If a node is marked as + // grounded, other nodes can then use it to decide their own state of + // grounded. + while ($hashNeeded) { + $startingGroundedNodes = count($hashes); + foreach ($nodes as $node) { + if (!isset($hashes[$node])) { + $hash = self::nodeHashFor($node, $statements, $hashes); + if (self::nodeIsGrounded($node, $statements, $hashes)) { + $hashes[$node] = $hash; + } + } + $ungroundedHashes[$node] = $hash; + } + + // after going over the list, any nodes with a unique hash can be marked + // as grounded, even if we have not tied them back to a root yet. + $uniques = array(); + foreach ($ungroundedHashes as $node => $hash) { + $uniques[$hash] = isset($uniques[$hash]) ? false : $node; + } + foreach ($uniques as $hash => $node) { + if ($node) { + $hashes[$node] = $hash; + } + } + $hashNeeded = ($startingGroundedNodes != count($hashes)); + } + + return array($hashes, $ungroundedHashes); + } + + /** + * Generate a hash for a node based on the signature of the statements it + * appears in. Signatures consist of grounded elements in statements + * associated with a node, that is, anything but an ungrounded anonymous + * node. Creating the hash is simply hashing a sorted list of each + * statement's signature, which is itself a concatenation of the string form + * of all grounded elements. + * + * Nodes other than the given node are considered grounded if they are a + * member in the given hash. + * + * Returns a tuple consisting of grounded being true or false and the string + * for the hash + * + * @ignore + */ + private static function nodeHashFor($node, $statements, $hashes) + { + $statement_signatures = array(); + foreach ($statements as $statement) { + foreach ($statement as $n) { + if ($n['type'] != 'literal' and $n['value'] == $node) { + array_push( + $statement_signatures, + self::hashStringFor($statement, $hashes, $node) + ); + } + } + } + + // Note that we sort the signatures--without a canonical ordering, + // we might get different hashes for equivalent nodes + sort($statement_signatures); + + // Convert statements into one long string and hash it + return sha1(implode('', $statement_signatures)); + } + + /** + * Returns true if a given node is grounded + * A node is groundd if it is not a blank node or it is included + * in the given mapping of grounded nodes. + * + * @ignore + */ + private static function nodeIsGrounded($node, $statements, $hashes) + { + $grounded = true; + foreach ($statements as $statement) { + if (in_array($node, $statement)) { + foreach ($statement as $resource) { + if ($node['type'] != 'bnode' or + isset($hashes[$node['value']]) or + $resource == $node + ) { + $grounded = false; + } + } + } + } + return $grounded; + } + + /** + * Provide a string signature for the given statement, collecting + * string signatures for grounded node elements. + * + * @ignore + */ + private static function hashStringFor($statement, $hashes, $node) + { + $str = ""; + foreach ($statement as $r) { + $str .= self::stringForNode($r, $hashes, $node); + } + return $str; + } + + /** + * Provides a string for the given node for use in a string signature + * Non-anonymous nodes will return their string form. Grounded anonymous + * nodes will return their hashed form. + * + * @ignore + */ + private static function stringForNode($node, $hashes, $target) + { + if (is_null($node)) { + return ""; + } elseif ($node['type'] == 'bnode') { + if ($node['value'] == $target) { + return "itself"; + } elseif (isset($hashes[$node['value']])) { + return $hashes[$node['value']]; + } else { + return "a blank node"; + } + } else { + $s = new Serialiser\Ntriples(); + return $s->serialiseValue($node); + } + } +} diff --git a/lib/Literal.php b/lib/Literal.php new file mode 100644 index 0000000..186777e --- /dev/null +++ b/lib/Literal.php @@ -0,0 +1,342 @@ +value = $value; + $this->lang = $lang ? $lang : null; + $this->datatype = $datatype ? $datatype : null; + + if ($this->datatype) { + if (is_object($this->datatype)) { + // Convert objects to strings + $this->datatype = strval($this->datatype); + } else { + // Expand shortened URIs (CURIEs) + $this->datatype = RdfNamespace::expand($this->datatype); + } + + // Literals can not have both a language and a datatype + $this->lang = null; + } else { + // Set the datatype based on the subclass + $class = get_class($this); + if (isset(self::$classMap[$class])) { + $this->datatype = self::$classMap[$class]; + $this->lang = null; + } + } + + if (is_float($this->value)) { + // special handling of floats, as they suffer from locale [mis]configuration + $this->value = rtrim(sprintf('%F', $this->value), '0'); + } else { + // Cast value to string + settype($this->value, 'string'); + } + } + + /** Returns the value of the literal. + * + * @return string Value of this literal. + */ + public function getValue() + { + return $this->value; + } + + /** Returns the full datatype URI of the literal. + * + * @return string Datatype URI of this literal. + */ + public function getDatatypeUri() + { + return $this->datatype; + } + + /** Returns the shortened datatype URI of the literal. + * + * @return string Datatype of this literal (e.g. xsd:integer). + */ + public function getDatatype() + { + if ($this->datatype) { + return RdfNamespace::shorten($this->datatype); + } else { + return null; + } + } + + /** Returns the language of the literal. + * + * @return string Language of this literal. + */ + public function getLang() + { + return $this->lang; + } + + /** Returns the properties of the literal as an associative array + * + * For example: + * array('type' => 'literal', 'value' => 'string value') + * + * @return array The properties of the literal + */ + public function toRdfPhp() + { + $array = array( + 'type' => 'literal', + 'value' => $this->value + ); + + if ($this->datatype) { + $array['datatype'] = $this->datatype; + } + + if ($this->lang) { + $array['lang'] = $this->lang; + } + + return $array; + } + + /** Magic method to return the value of a literal as a string + * + * @return string The value of the literal + */ + public function __toString() + { + return isset($this->value) ? $this->value : ''; + } + + /** Return pretty-print view of the literal + * + * @param string $format Either 'html' or 'text' + * @param string $color The colour of the text + * + * @return string + */ + public function dumpValue($format = 'html', $color = 'black') + { + return Utils::dumpLiteralValue($this, $format, $color); + } +} + +/* + Register default set of datatype classes +*/ + +Literal::setDatatypeMapping('xsd:boolean', 'EasyRdf\Literal\Boolean'); +Literal::setDatatypeMapping('xsd:date', 'EasyRdf\Literal\Date'); +Literal::setDatatypeMapping('xsd:dateTime', 'EasyRdf\Literal\DateTime'); +Literal::setDatatypeMapping('xsd:decimal', 'EasyRdf\Literal\Decimal'); +Literal::setDatatypeMapping('xsd:hexBinary', 'EasyRdf\Literal\HexBinary'); +Literal::setDatatypeMapping('rdf:HTML', 'EasyRdf\Literal\HTML'); +Literal::setDatatypeMapping('xsd:integer', 'EasyRdf\Literal\Integer'); +Literal::setDatatypeMapping('rdf:XMLLiteral', 'EasyRdf\Literal\XML'); diff --git a/lib/Literal/Boolean.php b/lib/Literal/Boolean.php new file mode 100644 index 0000000..1e73ac8 --- /dev/null +++ b/lib/Literal/Boolean.php @@ -0,0 +1,94 @@ +value) === 'true' or $this->value === '1'; + } + + /** Return true if the value of the literal is 'true' or '1' + * + * @return bool + */ + public function isTrue() + { + return strtolower($this->value) === 'true' or $this->value === '1'; + } + + /** Return true if the value of the literal is 'false' or '0' + * + * @return bool + */ + public function isFalse() + { + return strtolower($this->value) === 'false' or $this->value === '0'; + } +} diff --git a/lib/Literal/Date.php b/lib/Literal/Date.php new file mode 100644 index 0000000..756f4d1 --- /dev/null +++ b/lib/Literal/Date.php @@ -0,0 +1,140 @@ +format('Y-m-d'); + } + + parent::__construct($value, null, $datatype); + } + + /** Parses a string using DateTime and creates a new literal + * + * Example: + * $date = EasyRdf\Literal\Date::parse('1 January 2011'); + * + * @see DateTime + * @param string $value The date to parse + * + * @return self + */ + public static function parse($value) + { + $value = new \DateTime($value); + return new self($value); + } + + /** Returns the date as a PHP DateTime object + * + * @see DateTime::format + * @return string + */ + public function getValue() + { + return new \DateTime($this->value); + } + + /** Returns date formatted according to given format + * + * @see DateTime::format + * @param string $format + * + * @return string + */ + public function format($format) + { + return $this->getValue()->format($format); + } + + /** A full integer representation of the year, 4 digits + * + * @return integer + */ + public function year() + { + return (int)$this->format('Y'); + } + + /** Integer representation of the month + * + * @return integer + */ + public function month() + { + return (int)$this->format('m'); + } + + /** Integer representation of the day of the month + * + * @return integer + */ + public function day() + { + return (int)$this->format('d'); + } +} diff --git a/lib/Literal/DateTime.php b/lib/Literal/DateTime.php new file mode 100644 index 0000000..467f60a --- /dev/null +++ b/lib/Literal/DateTime.php @@ -0,0 +1,119 @@ +format(\DateTime::ATOM); + $value = preg_replace('/[\+\-]00(\:?)00$/', 'Z', $atom); + } + + Literal::__construct($value, null, $datatype); + } + + /** Parses a string using DateTime and creates a new literal + * + * Example: + * $dt = EasyRdf\Literal\DateTime::parse('Mon 18 Jul 2011 18:45:43 BST'); + * + * @see DateTime + * @param string $value The date and time to parse + * + * @return self + */ + public static function parse($value) + { + $value = new \DateTime($value); + return new self($value); + } + + /** 24-hour format of the hour as an integer + * + * @return integer + */ + public function hour() + { + return (int)$this->format('H'); + } + + /** The minutes pasts the hour as an integer + * + * @return integer + */ + public function min() + { + return (int)$this->format('i'); + } + + /** The seconds pasts the minute as an integer + * + * @return integer + */ + public function sec() + { + return (int)$this->format('s'); + } +} diff --git a/lib/Literal/Decimal.php b/lib/Literal/Decimal.php new file mode 100644 index 0000000..4e34b10 --- /dev/null +++ b/lib/Literal/Decimal.php @@ -0,0 +1,127 @@ +value); + } + + /** + * @param string $value + * + * @throws \UnexpectedValueException + */ + public static function validate($value) + { + if (!mb_ereg_match(self::DECIMAL_REGEX, $value)) { + throw new \UnexpectedValueException("'{$value}' doesn't look like a valid decimal"); + } + } + + /** + * Converts valid xsd:decimal literal to Canonical representation + * see http://www.w3.org/TR/xmlschema-2/#decimal + * + * @param string $value Valid xsd:decimal literal + * + * @return string + */ + public static function canonicalise($value) + { + $pieces = array(); + mb_ereg(self::DECIMAL_REGEX, $value, $pieces); + + $sign = $pieces[1] === '-' ? '-' : ''; // '+' is not allowed + $integer = ltrim(($pieces[4] !== false) ? $pieces[4] : $pieces[7], '0'); + $fractional = rtrim($pieces[5], '0'); + + if (empty($integer)) { + $integer = '0'; + } + + if (empty($fractional)) { + $fractional = '0'; + } + + return "{$sign}{$integer}.{$fractional}"; + } +} diff --git a/lib/Literal/HTML.php b/lib/Literal/HTML.php new file mode 100644 index 0000000..fdf0157 --- /dev/null +++ b/lib/Literal/HTML.php @@ -0,0 +1,72 @@ +value, $allowableTags); + } +} diff --git a/lib/Literal/HexBinary.php b/lib/Literal/HexBinary.php new file mode 100644 index 0000000..393c5c9 --- /dev/null +++ b/lib/Literal/HexBinary.php @@ -0,0 +1,93 @@ +value); + } +} diff --git a/lib/Literal/Integer.php b/lib/Literal/Integer.php new file mode 100644 index 0000000..0ae3330 --- /dev/null +++ b/lib/Literal/Integer.php @@ -0,0 +1,69 @@ +value; + } +} diff --git a/lib/Literal/XML.php b/lib/Literal/XML.php new file mode 100644 index 0000000..55d2b74 --- /dev/null +++ b/lib/Literal/XML.php @@ -0,0 +1,72 @@ +loadXML($this->value); + return $dom; + } +} diff --git a/lib/ParsedUri.php b/lib/ParsedUri.php new file mode 100644 index 0000000..679dadf --- /dev/null +++ b/lib/ParsedUri.php @@ -0,0 +1,340 @@ +scheme = isset($matches[2]) ? $matches[2] : ''; + } + if (!empty($matches[3])) { + $this->authority = isset($matches[4]) ? $matches[4] : ''; + } + $this->path = isset($matches[5]) ? $matches[5] : ''; + if (!empty($matches[6])) { + $this->query = isset($matches[7]) ? $matches[7] : ''; + } + if (!empty($matches[8])) { + $this->fragment = isset($matches[9]) ? $matches[9] : ''; + } + } + } elseif (is_array($uri)) { + $this->scheme = isset($uri['scheme']) ? $uri['scheme'] : null; + $this->authority = isset($uri['authority']) ? $uri['authority'] : null; + $this->path = isset($uri['path']) ? $uri['path'] : null; + $this->query = isset($uri['query']) ? $uri['query'] : null; + $this->fragment = isset($uri['fragment']) ? $uri['fragment'] : null; + } + } + + + /** Returns true if this is an absolute (complete) URI + * @return boolean + */ + public function isAbsolute() + { + return $this->scheme !== null; + } + + /** Returns true if this is an relative (partial) URI + * @return boolean + */ + public function isRelative() + { + return $this->scheme === null; + } + + /** Returns the scheme of the URI (e.g. http) + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + + /** Sets the scheme of the URI (e.g. http) + * @param string $scheme The new value for the scheme of the URI + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + } + + /** Returns the authority of the URI (e.g. www.example.com:8080) + * @return string + */ + public function getAuthority() + { + return $this->authority; + } + + /** Sets the authority of the URI (e.g. www.example.com:8080) + * @param string $authority The new value for the authority component of the URI + */ + public function setAuthority($authority) + { + $this->authority = $authority; + } + + /** Returns the path of the URI (e.g. /foo/bar) + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** Set the path of the URI (e.g. /foo/bar) + * @param string $path The new value for the path component of the URI + */ + public function setPath($path) + { + $this->path = $path; + } + + /** Returns the query string part of the URI (e.g. foo=bar) + * @return string + */ + public function getQuery() + { + return $this->query; + } + + /** Set the query string of the URI (e.g. foo=bar) + * @param string $query The new value for the query string component of the URI + */ + public function setQuery($query) + { + $this->query = $query; + } + + /** Returns the fragment part of the URI (i.e. after the #) + * @return string + */ + public function getFragment() + { + return $this->fragment; + } + + /** Set the fragment of the URI (i.e. after the #) + * @param string $fragment The new value for the fragment component of the URI + */ + public function setFragment($fragment) + { + $this->fragment = $fragment; + } + + + /** + * Normalises the path of this URI if it has one. + * + * Normalising a path means that any unnecessary '.' and '..' segments are removed. For example, the + * URI http://example.com/a/b/../c/./d would be normalised to http://example.com/a/c/d + * + * @return self + */ + public function normalise() + { + if (empty($this->path)) { + return $this; + } + + // Remove ./ from the start + if (substr($this->path, 0, 2) == './') { + // Remove both characters + $this->path = substr($this->path, 2); + } + + // Remove /. from the end + if (substr($this->path, -2) == '/.') { + // Remove only the last dot, not the slash! + $this->path = substr($this->path, 0, -1); + } + + if (substr($this->path, -3) == '/..') { + $this->path .= '/'; + } + + // Split the path into its segments + $segments = explode('/', $this->path); + $newSegments = array(); + + // Remove all unnecessary '.' and '..' segments + foreach ($segments as $segment) { + if ($segment == '..') { + // Remove the previous part of the path + $count = count($newSegments); + if ($count > 0 && $newSegments[$count-1]) { + array_pop($newSegments); + } + } elseif ($segment == '.') { + // Ignore + continue; + } else { + array_push($newSegments, $segment); + } + } + + // Construct the new normalised path + $this->path = implode('/', $newSegments); + + // Allow easy chaining of methods + return $this; + } + + /** + * Resolves a relative URI using this URI as the base URI. + */ + public function resolve($relUri) + { + // If it is a string, then convert it to a parsed object + if (is_string($relUri)) { + $relUri = new self($relUri); + } + + // This code is based on the pseudocode in section 5.2.2 of RFC3986 + $target = new self(); + if ($relUri->scheme) { + $target->scheme = $relUri->scheme; + $target->authority = $relUri->authority; + $target->path = $relUri->path; + $target->query = $relUri->query; + } else { + if ($relUri->authority) { + $target->authority = $relUri->authority; + $target->path = $relUri->path; + $target->query = $relUri->query; + } else { + if (empty($relUri->path)) { + $target->path = $this->path; + if ($relUri->query) { + $target->query = $relUri->query; + } else { + $target->query = $this->query; + } + } else { + if (substr($relUri->path, 0, 1) == '/') { + $target->path = $relUri->path; + } else { + $path = $this->path; + $lastSlash = strrpos($path, '/'); + if ($lastSlash !== false) { + $path = substr($path, 0, $lastSlash + 1); + } else { + $path = '/'; + } + + $target->path .= $path . $relUri->path; + } + $target->query = $relUri->query; + } + $target->authority = $this->authority; + } + $target->scheme = $this->scheme; + } + + $target->fragment = $relUri->fragment; + + $target->normalise(); + + return $target; + } + + /** Convert the parsed URI back into a string + * + * @return string The URI as a string + */ + public function toString() + { + $str = ''; + if ($this->scheme !== null) { + $str .= $this->scheme . ':'; + } + if ($this->authority !== null) { + $str .= '//' . $this->authority; + } + $str .= $this->path; + if ($this->query !== null) { + $str .= '?' . $this->query; + } + if ($this->fragment !== null) { + $str .= '#' . $this->fragment; + } + return $str; + } + + /** Magic method to convert the URI, when casted, back to a string + * + * @return string The URI as a string + */ + public function __toString() + { + return $this->toString(); + } +} diff --git a/lib/Parser.php b/lib/Parser.php new file mode 100644 index 0000000..8b72017 --- /dev/null +++ b/lib/Parser.php @@ -0,0 +1,154 @@ +bnodeMap[$name])) { + $this->bnodeMap[$name] = $this->graph->newBNodeId(); + } + return $this->bnodeMap[$name]; + } + + /** + * Delete the bnode mapping - to be called at the start of a new parse + * @ignore + */ + protected function resetBnodeMap() + { + $this->bnodeMap = array(); + } + + /** + * Check, cleanup parameters and prepare for parsing + * @ignore + */ + protected function checkParseParams($graph, $data, $format, $baseUri) + { + if ($graph == null or !is_object($graph) or + !($graph instanceof Graph)) { + throw new \InvalidArgumentException( + '$graph should be an EasyRdf\Graph object and cannot be null' + ); + } else { + $this->graph = $graph; + } + + if ($format == null or $format == '') { + throw new \InvalidArgumentException( + "\$format cannot be null or empty" + ); + } elseif (is_object($format) and $format instanceof Format) { + $this->format = $format = $format->getName(); + } elseif (!is_string($format)) { + throw new \InvalidArgumentException( + '$format should be a string or an EasyRdf\Format object' + ); + } else { + $this->format = $format; + } + + if ($baseUri) { + if (!is_string($baseUri)) { + throw new \InvalidArgumentException( + "\$baseUri should be a string" + ); + } else { + $this->baseUri = new ParsedUri($baseUri); + } + } else { + $this->baseUri = null; + } + + // Prepare for parsing + $this->resetBnodeMap(); + $this->tripleCount = 0; + } + + /** + * Sub-classes must follow this protocol + * @ignore + */ + public function parse($graph, $data, $format, $baseUri) + { + throw new Exception( + "This method should be overridden by sub-classes." + ); + } + + /** + * Add a triple to the current graph, and keep count of the number of triples + * @ignore + */ + protected function addTriple($resource, $property, $value) + { + $count = $this->graph->add($resource, $property, $value); + $this->tripleCount += $count; + return $count; + } +} diff --git a/lib/Parser/Arc.php b/lib/Parser/Arc.php new file mode 100644 index 0000000..d9a3c44 --- /dev/null +++ b/lib/Parser/Arc.php @@ -0,0 +1,99 @@ + 'RDFXML', + 'turtle' => 'Turtle', + 'ntriples' => 'Turtle', + 'rdfa' => 'SemHTML', + ); + + /** + * Constructor + */ + public function __construct() + { + if (!class_exists('ARC2')) { + throw new \EasyRdf\Exception('ARC2 dependency is not installed'); + } + } + + /** + * Parse an RDF document into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + if (array_key_exists($format, self::$supportedTypes)) { + $className = self::$supportedTypes[$format]; + } else { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\Arc does not support: {$format}" + ); + } + + $parser = \ARC2::getParser($className); + if ($parser) { + $parser->parse($baseUri, $data); + $rdfphp = $parser->getSimpleIndex(false); + return parent::parse($graph, $rdfphp, 'php', $baseUri); + } else { + throw new \EasyRdf\Exception( + "ARC2 failed to get a $className parser." + ); + } + } +} diff --git a/lib/Parser/Exception.php b/lib/Parser/Exception.php new file mode 100644 index 0000000..46e5794 --- /dev/null +++ b/lib/Parser/Exception.php @@ -0,0 +1,77 @@ +parserLine = $line; + $this->parserColumn = $column; + + if (!is_null($line)) { + $message .= " on line $line"; + if (!is_null($column)) { + $message .= ", column $column"; + } + } + + parent::__construct($message); + } + + public function getParserLine() + { + return $this->parserLine; + } + + public function getParserColumn() + { + return $this->parserColumn; + } +} diff --git a/lib/Parser/Json.php b/lib/Parser/Json.php new file mode 100644 index 0000000..79f17f2 --- /dev/null +++ b/lib/Parser/Json.php @@ -0,0 +1,158 @@ +jsonLastErrorExists = function_exists('json_last_error'); + } + + /** Return the last JSON parser error as a string + * + * If json_last_error() is not available a generic message will be returned. + * + * @ignore + */ + protected function jsonLastErrorString() + { + if ($this->jsonLastErrorExists) { + switch (json_last_error()) { + case JSON_ERROR_NONE: + return null; + case JSON_ERROR_DEPTH: + return "JSON Parse error: the maximum stack depth has been exceeded"; + case JSON_ERROR_STATE_MISMATCH: + return "JSON Parse error: invalid or malformed JSON"; + case JSON_ERROR_CTRL_CHAR: + return "JSON Parse error: control character error, possibly incorrectly encoded"; + case JSON_ERROR_SYNTAX: + return "JSON Parse syntax error"; + case JSON_ERROR_UTF8: + return "JSON Parse error: malformed UTF-8 characters, possibly incorrectly encoded"; + default: + return "JSON Parse error: unknown"; + } + } else { + return "JSON Parse error"; + } + } + + /** Parse the triple-centric JSON format, as output by libraptor + * + * http://librdf.org/raptor/api/serializer-json.html + * + * @ignore + */ + protected function parseJsonTriples($data, $baseUri) + { + foreach ($data['triples'] as $triple) { + if ($triple['subject']['type'] == 'bnode') { + $subject = $this->remapBnode($triple['subject']['value']); + } else { + $subject = $triple['subject']['value']; + } + + $predicate = $triple['predicate']['value']; + + if ($triple['object']['type'] == 'bnode') { + $object = array( + 'type' => 'bnode', + 'value' => $this->remapBnode($triple['object']['value']) + ); + } else { + $object = $triple['object']; + } + + $this->addTriple($subject, $predicate, $object); + } + + return $this->tripleCount; + } + + /** + * Parse RDF/JSON into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws Exception + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + $this->checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'json') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\Json does not support: {$format}" + ); + } + + $decoded = @json_decode(strval($data), true); + if ($decoded === null) { + throw new Exception( + $this->jsonLastErrorString() + ); + } + + if (array_key_exists('triples', $decoded)) { + return $this->parseJsonTriples($decoded, $baseUri); + } else { + return parent::parse($graph, $decoded, 'php', $baseUri); + } + } +} diff --git a/lib/Parser/JsonLd.php b/lib/Parser/JsonLd.php new file mode 100644 index 0000000..c5418d9 --- /dev/null +++ b/lib/Parser/JsonLd.php @@ -0,0 +1,127 @@ + + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class JsonLd extends Parser +{ + /** + * Parse a JSON-LD document into an EasyRdf\Graph + * + * Attention: Since JSON-LD supports datasets, a document may contain + * multiple graphs and not just one. This parser returns only the + * default graph. An alternative would be to merge all graphs. + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws Exception + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'jsonld') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\JsonLd does not support {$format}" + ); + } + + try { + $quads = LD\JsonLD::toRdf($data, array('base' => $baseUri)); + } catch (LD\Exception\JsonLdException $e) { + throw new Exception($e->getMessage()); + } + + foreach ($quads as $quad) { + // Ignore named graphs + if (null !== $quad->getGraph()) { + continue; + } + + $subject = (string) $quad->getSubject(); + if ('_:' === substr($subject, 0, 2)) { + $subject = $this->remapBnode($subject); + } + + $predicate = (string) $quad->getProperty(); + + if ($quad->getObject() instanceof \ML\IRI\IRI) { + $object = array( + 'type' => 'uri', + 'value' => (string) $quad->getObject() + ); + + if ('_:' === substr($object['value'], 0, 2)) { + $object = array( + 'type' => 'bnode', + 'value' => $this->remapBnode($object['value']) + ); + } + } else { + $object = array( + 'type' => 'literal', + 'value' => $quad->getObject()->getValue() + ); + + if ($quad->getObject() instanceof LD\LanguageTaggedString) { + $object['lang'] = $quad->getObject()->getLanguage(); + } else { + $object['datatype'] = $quad->getObject()->getType(); + } + } + + $this->addTriple($subject, $predicate, $object); + } + + return $this->tripleCount; + } +} diff --git a/lib/Parser/Ntriples.php b/lib/Parser/Ntriples.php new file mode 100644 index 0000000..4a6b3cd --- /dev/null +++ b/lib/Parser/Ntriples.php @@ -0,0 +1,218 @@ + chr(0x09), + 'b' => chr(0x08), + 'n' => chr(0x0A), + 'r' => chr(0x0D), + 'f' => chr(0x0C), + '\"' => chr(0x22), + '\'' => chr(0x27) + ); + foreach ($mappings as $in => $out) { + $str = preg_replace('/\x5c([' . $in . '])/', $out, $str); + } + + if (stripos($str, '\u') === false) { + return $str; + } + + while (preg_match('/\\\(U)([0-9A-F]{8})/', $str, $matches) || + preg_match('/\\\(u)([0-9A-F]{4})/', $str, $matches)) { + $no = hexdec($matches[2]); + if ($no < 128) { // 0x80 + $char = chr($no); + } elseif ($no < 2048) { // 0x800 + $char = chr(($no >> 6) + 192) . + chr(($no & 63) + 128); + } elseif ($no < 65536) { // 0x10000 + $char = chr(($no >> 12) + 224) . + chr((($no >> 6) & 63) + 128) . + chr(($no & 63) + 128); + } elseif ($no < 2097152) { // 0x200000 + $char = chr(($no >> 18) + 240) . + chr((($no >> 12) & 63) + 128) . + chr((($no >> 6) & 63) + 128) . + chr(($no & 63) + 128); + } else { + # FIXME: throw an exception instead? + $char = ''; + } + $str = str_replace('\\' . $matches[1] . $matches[2], $char, $str); + } + return $str; + } + + /** + * @ignore + */ + protected function parseNtriplesSubject($sub, $lineNum) + { + if (preg_match('/<([^<>]+)>/', $sub, $matches)) { + return $this->unescapeString($matches[1]); + } elseif (preg_match('/_:([A-Za-z0-9]*)/', $sub, $matches)) { + if (empty($matches[1])) { + return $this->graph->newBNodeId(); + } else { + $nodeid = $this->unescapeString($matches[1]); + return $this->remapBnode($nodeid); + } + } else { + throw new Exception( + "Failed to parse subject: $sub", + $lineNum + ); + } + } + + /** + * @ignore + */ + protected function parseNtriplesObject($obj, $lineNum) + { + if (preg_match('/"(.+)"\^\^<([^<>]+)>/', $obj, $matches)) { + return array( + 'type' => 'literal', + 'value' => $this->unescapeString($matches[1]), + 'datatype' => $this->unescapeString($matches[2]) + ); + } elseif (preg_match('/"(.+)"@([\w\-]+)/', $obj, $matches)) { + return array( + 'type' => 'literal', + 'value' => $this->unescapeString($matches[1]), + 'lang' => $this->unescapeString($matches[2]) + ); + } elseif (preg_match('/"(.*)"/', $obj, $matches)) { + return array('type' => 'literal', 'value' => $this->unescapeString($matches[1])); + } elseif (preg_match('/<([^<>]+)>/', $obj, $matches)) { + return array('type' => 'uri', 'value' => $this->unescapeString($matches[1])); + } elseif (preg_match('/_:([A-Za-z0-9]*)/', $obj, $matches)) { + if (empty($matches[1])) { + return array( + 'type' => 'bnode', + 'value' => $this->graph->newBNodeId() + ); + } else { + $nodeid = $this->unescapeString($matches[1]); + return array( + 'type' => 'bnode', + 'value' => $this->remapBnode($nodeid) + ); + } + } else { + throw new Exception( + "Failed to parse object: $obj", + $lineNum + ); + } + } + + /** + * Parse an N-Triples document into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws Exception + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'ntriples') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\Ntriples does not support: $format" + ); + } + + $lines = preg_split('/\x0D?\x0A/', strval($data)); + foreach ($lines as $index => $line) { + $lineNum = $index + 1; + if (preg_match('/^\s*#/', $line)) { + # Comment + continue; + } elseif (preg_match('/^\s*(.+?)\s+<([^<>]+?)>\s+(.+?)\s*\.\s*$/', $line, $matches)) { + $this->addTriple( + $this->parseNtriplesSubject($matches[1], $lineNum), + $this->unescapeString($matches[2]), + $this->parseNtriplesObject($matches[3], $lineNum) + ); + } elseif (preg_match('/^\s*$/', $line)) { + # Blank line + continue; + } else { + throw new Exception( + "Failed to parse statement", + $lineNum + ); + } + } + + return $this->tripleCount; + } +} diff --git a/lib/Parser/Rapper.php b/lib/Parser/Rapper.php new file mode 100644 index 0000000..1c9ad3c --- /dev/null +++ b/lib/Parser/Rapper.php @@ -0,0 +1,107 @@ +/dev/null", $output, $status); + if ($status != 0) { + throw new \EasyRdf\Exception( + "Failed to execute the command '$rapperCmd': " . join("\n", $output) + ); + } elseif (version_compare($output[0], self::MINIMUM_RAPPER_VERSION) < 0) { + throw new \EasyRdf\Exception( + "Version ".self::MINIMUM_RAPPER_VERSION." or higher of rapper is required." + ); + } else { + $this->rapperCmd = $rapperCmd; + } + } + + /** + * Parse an RDF document into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + $json = Utils::execCommandPipe( + $this->rapperCmd, + array( + '--quiet', + '--input', $format, + '--output', 'json', + '--ignore-errors', + '--input-uri', $baseUri, + '--output-uri', '-', '-' + ), + $data + ); + + // Parse in the JSON + return parent::parse($graph, $json, 'json', $baseUri); + } +} diff --git a/lib/Parser/RdfPhp.php b/lib/Parser/RdfPhp.php new file mode 100644 index 0000000..a4278e3 --- /dev/null +++ b/lib/Parser/RdfPhp.php @@ -0,0 +1,134 @@ +checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'php') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\RdfPhp does not support: $format" + ); + } + + if (!is_array($data)) { + throw new Exception('expected array, got '.gettype($data)); + } + + foreach ($data as $orig_subject => $properties) { + if (is_int($orig_subject)) { + throw new Exception('expected array indexed by IRIs, got list'); + } + + if (substr($orig_subject, 0, 2) === '_:') { + $subject = $this->remapBnode($orig_subject); + } elseif (preg_match('/^\w+$/', $orig_subject)) { + # Cope with invalid RDF/JSON serialisations that + # put the node name in, without the _: prefix + # (such as net.fortytwo.sesametools.rdfjson) + $subject = $this->remapBnode($orig_subject); + } else { + $subject = $orig_subject; + } + + if (!is_array($properties)) { + throw new Exception("expected array as value of '{$orig_subject}' key, got ".gettype($properties)); + } + + foreach ($properties as $property => $objects) { + if (is_int($property)) { + throw new Exception("expected 'array indexed by IRIs' as value of '{$orig_subject}' key, got list"); + } + + if (!is_array($objects)) { + throw new Exception( + "expected list of objects as value of '{$orig_subject}' -> '{$property}' node, got ". + gettype($objects) + ); + } + + foreach ($objects as $i => $object) { + if (!is_array($object) or !isset($object['type']) or !isset($object['value'])) { + throw new Exception( + "expected array with 'type' and 'value' keys as value of ". + "'{$orig_subject}' -> '{$property}' -> '{$i}' node" + ); + } + + if ($object['type'] === 'bnode') { + $object['value'] = $this->remapBnode($object['value']); + } + $this->addTriple($subject, $property, $object); + } + } + } + + return $this->tripleCount; + } +} diff --git a/lib/Parser/RdfXml.php b/lib/Parser/RdfXml.php new file mode 100644 index 0000000..5b47a7a --- /dev/null +++ b/lib/Parser/RdfXml.php @@ -0,0 +1,815 @@ +graph = $graph; + $this->state = 0; + $this->xLang = null; + $this->xBase = new ParsedUri($base); + $this->xml = 'http://www.w3.org/XML/1998/namespace'; + $this->rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + $this->nsp = array($this->xml => 'xml', $this->rdf => 'rdf'); + $this->sStack = array(); + $this->sCount = 0; + } + + /** @ignore */ + protected function initXMLParser() + { + if (!isset($this->xmlParser)) { + $parser = xml_parser_create_ns('UTF-8', ''); + xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); + xml_set_element_handler($parser, 'startElementHandler', 'endElementHandler'); + xml_set_character_data_handler($parser, 'cdataHandler'); + xml_set_start_namespace_decl_handler($parser, 'newNamespaceHandler'); + xml_set_object($parser, $this); + $this->xmlParser = $parser; + } + } + + /** @ignore */ + protected function pushS(&$s) + { + $s['pos'] = $this->sCount; + $this->sStack[$this->sCount] = $s; + $this->sCount++; + } + + /** @ignore */ + protected function popS() + { + $r = array(); + $this->sCount--; + for ($i = 0, $iMax = $this->sCount; $i < $iMax; $i++) { + $r[$i] = $this->sStack[$i]; + } + $this->sStack = $r; + } + + /** @ignore */ + protected function updateS($s) + { + $this->sStack[$s['pos']] = $s; + } + + /** @ignore */ + protected function getParentS() + { + if ($this->sCount && isset($this->sStack[$this->sCount - 1])) { + return $this->sStack[$this->sCount - 1]; + } else { + return false; + } + } + + /** @ignore */ + protected function getParentXBase() + { + if ($p = $this->getParentS()) { + if (isset($p['p_x_base']) && $p['p_x_base']) { + return $p['p_x_base']; + } elseif (isset($p['x_base'])) { + return $p['x_base']; + } else { + return ''; + } + } else { + return $this->xBase; + } + } + + /** @ignore */ + protected function getParentXLang() + { + if ($p = $this->getParentS()) { + if (isset($p['p_x_lang']) && $p['p_x_lang']) { + return $p['p_x_lang']; + } elseif (isset($p['x_lang'])) { + return $p['x_lang']; + } else { + return null; + } + } else { + return $this->xLang; + } + } + + /** @ignore */ + protected function splitURI($v) + { + /* auto-splitting on / or # */ + if (preg_match('/^(.*[\/\#])([^\/\#]+)$/', $v, $m)) { + return array($m[1], $m[2]); + } + /* auto-splitting on last special char, e.g. urn:foo:bar */ + if (preg_match('/^(.*[\:\/])([^\:\/]+)$/', $v, $m)) { + return array($m[1], $m[2]); + } + return array($v, ''); + } + + /** @ignore */ + protected function add($s, $p, $o, $sType, $oType, $oDatatype = null, $oLang = null) + { + $this->addTriple( + $s, + $p, + array( + 'type' => $oType, + 'value' => $o, + 'lang' => $oLang, + 'datatype' => $oDatatype + ) + ); + } + + /** @ignore */ + protected function reify($t, $s, $p, $o, $sType, $oType, $oDatatype = null, $oLang = null) + { + $this->add($t, $this->rdf.'type', $this->rdf.'Statement', 'uri', 'uri'); + $this->add($t, $this->rdf.'subject', $s, 'uri', $sType); + $this->add($t, $this->rdf.'predicate', $p, 'uri', 'uri'); + $this->add($t, $this->rdf.'object', $o, 'uri', $oType, $oDatatype, $oLang); + } + + /** @ignore */ + protected function startElementHandler($p, $t, $a) + { + switch ($this->state) { + case 0: + return $this->startState0($t, $a); + case 1: + return $this->startState1($t, $a); + case 2: + return $this->startState2($t, $a); + case 4: + return $this->startState4($t, $a); + case 5: + return $this->startState5($t, $a); + case 6: + return $this->startState6($t, $a); + default: + throw new Exception( + 'startElementHandler() called at state ' . $this->state . ' in '.$t + ); + } + } + + /** @ignore */ + protected function endElementHandler($p, $t) + { + switch ($this->state) { + case 1: + return $this->endState1($t); + case 2: + return $this->endState2($t); + case 3: + return $this->endState3($t); + case 4: + return $this->endState4($t); + case 5: + return $this->endState5($t); + case 6: + return $this->endState6($t); + default: + throw new Exception( + 'endElementHandler() called at state ' . $this->state . ' in '.$t + ); + } + } + + /** @ignore */ + protected function cdataHandler($p, $d) + { + switch ($this->state) { + case 4: + return $this->cdataState4($d); + case 6: + return $this->cdataState6($d); + default: + return false; + } + } + + /** @ignore */ + protected function newNamespaceHandler($p, $prf, $uri) + { + $this->nsp[$uri] = isset($this->nsp[$uri]) ? $this->nsp[$uri] : $prf; + } + + /** @ignore */ + protected function startState0($t, $a) + { + $this->state = 1; + if ($t !== $this->rdf.'RDF') { + $this->startState1($t, $a); + } else { + if (isset($a[$this->xml.'base'])) { + $this->xBase = $this->xBase->resolve($a[$this->xml.'base']); + } + } + } + + /** @ignore */ + protected function startState1($t, $a) + { + $s = array( + 'x_base' => $this->getParentXBase(), + 'x_lang' => $this->getParentXLang(), + 'li_count' => 0, + ); + + if (isset($a[$this->xml.'base'])) { + $s['x_base'] = $this->xBase->resolve($a[$this->xml.'base']); + } + + if (isset($a[$this->xml.'lang'])) { + $s['x_lang'] = $a[$this->xml.'lang']; + } + + /* ID */ + if (isset($a[$this->rdf.'ID'])) { + $s['type'] = 'uri'; + $s['value'] = $s['x_base']->resolve('#'.$a[$this->rdf.'ID']); + /* about */ + } elseif (isset($a[$this->rdf.'about'])) { + $s['type'] = 'uri'; + $s['value'] = $s['x_base']->resolve($a[$this->rdf.'about']); + /* bnode */ + } else { + $s['type'] = 'bnode'; + if (isset($a[$this->rdf.'nodeID'])) { + $s['value'] = $this->remapBnode($a[$this->rdf.'nodeID']); + } else { + $s['value'] = $this->graph->newBNodeId(); + } + } + + /* sub-node */ + if ($this->state === 4) { + $supS = $this->getParentS(); + /* new collection */ + if (isset($supS['o_is_coll']) && $supS['o_is_coll']) { + $coll = array( + 'type' => 'bnode', + 'value' => $this->graph->newBNodeId(), + 'is_coll' => true, + 'x_base' => $s['x_base'], + 'x_lang' => $s['x_lang'] + ); + $this->add($supS['value'], $supS['p'], $coll['value'], $supS['type'], $coll['type']); + $this->add($coll['value'], $this->rdf.'first', $s['value'], $coll['type'], $s['type']); + $this->pushS($coll); + } elseif (isset($supS['is_coll']) && $supS['is_coll']) { + /* new entry in existing coll */ + $coll = array( + 'type' => 'bnode', + 'value' => $this->graph->newBNodeId(), + 'is_coll' => true, + 'x_base' => $s['x_base'], + 'x_lang' => $s['x_lang'] + ); + $this->add($supS['value'], $this->rdf.'rest', $coll['value'], $supS['type'], $coll['type']); + $this->add($coll['value'], $this->rdf.'first', $s['value'], $coll['type'], $s['type']); + $this->pushS($coll); + /* normal sub-node */ + } elseif (isset($supS['p']) && $supS['p']) { + $this->add($supS['value'], $supS['p'], $s['value'], $supS['type'], $s['type']); + } + } + /* typed node */ + if ($t !== $this->rdf.'Description') { + $this->add($s['value'], $this->rdf.'type', $t, $s['type'], 'uri'); + } + /* (additional) typing attr */ + if (isset($a[$this->rdf.'type'])) { + $this->add($s['value'], $this->rdf.'type', $a[$this->rdf.'type'], $s['type'], 'uri'); + } + + /* Seq|Bag|Alt */ + // if (in_array($t, array($this->rdf.'Seq', $this->rdf.'Bag', $this->rdf.'Alt'))) { + // # FIXME: what is this? + // $s['is_con'] = true; + // } + + /* any other attrs (skip rdf and xml, except rdf:_, rdf:value, rdf:Seq) */ + foreach ($a as $k => $v) { + if (((strpos($k, $this->xml) === false) && (strpos($k, $this->rdf) === false)) || + preg_match('/(\_[0-9]+|value|Seq|Bag|Alt|Statement|Property|List)$/', $k)) { + if (strpos($k, ':')) { + $this->add($s['value'], $k, $v, $s['type'], 'literal', null, $s['x_lang']); + } + } + } + $this->pushS($s); + $this->state = 2; + } + + /** @ignore */ + protected function startState2($t, $a) + { + $s = $this->getParentS(); + foreach (array('p_x_base', 'p_x_lang', 'p_id', 'o_is_coll') as $k) { + unset($s[$k]); + } + /* base */ + if (isset($a[$this->xml.'base'])) { + $s['p_x_base'] = $s['x_base']->resolve($a[$this->xml.'base']); + } + $b = isset($s['p_x_base']) && $s['p_x_base'] ? $s['p_x_base'] : $s['x_base']; + /* lang */ + if (isset($a[$this->xml.'lang'])) { + $s['p_x_lang'] = $a[$this->xml.'lang']; + } + $l = isset($s['p_x_lang']) && $s['p_x_lang'] ? $s['p_x_lang'] : $s['x_lang']; + /* adjust li */ + if ($t === $this->rdf.'li') { + $s['li_count']++; + $t = $this->rdf.'_'.$s['li_count']; + } + /* set p */ + $s['p'] = $t; + /* reification */ + if (isset($a[$this->rdf.'ID'])) { + $s['p_id'] = $a[$this->rdf.'ID']; + } + $o = array('value' => null, 'type' => null, 'x_base' => $b, 'x_lang' => $l); + /* resource/rdf:resource */ + if (isset($a['resource'])) { + $a[$this->rdf.'resource'] = $a['resource']; + unset($a['resource']); + } + if (isset($a[$this->rdf.'resource'])) { + $o['type'] = 'uri'; + $o['value'] = $b->resolve($a[$this->rdf.'resource']); + $this->add($s['value'], $s['p'], $o['value'], $s['type'], $o['type']); + /* type */ + if (isset($a[$this->rdf.'type'])) { + $this->add( + $o['value'], + $this->rdf.'type', + $a[$this->rdf.'type'], + 'uri', + 'uri' + ); + } + /* reification */ + if (isset($s['p_id'])) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'] + ); + unset($s['p_id']); + } + $this->state = 3; + } elseif (isset($a[$this->rdf.'nodeID'])) { + /* named bnode */ + $o['value'] = $this->remapBnode($a[$this->rdf.'nodeID']); + $o['type'] = 'bnode'; + $this->add($s['value'], $s['p'], $o['value'], $s['type'], $o['type']); + $this->state = 3; + /* reification */ + if (isset($s['p_id'])) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'] + ); + } + /* parseType */ + } elseif (isset($a[$this->rdf.'parseType'])) { + if ($a[$this->rdf.'parseType'] === 'Literal') { + $s['o_xml_level'] = 0; + $s['o_xml_data'] = ''; + $s['p_xml_literal_level'] = 0; + $s['ns'] = array(); + $this->state = 6; + } elseif ($a[$this->rdf.'parseType'] === 'Resource') { + $o['value'] = $this->graph->newBNodeId(); + $o['type'] = 'bnode'; + $o['hasClosingTag'] = 0; + $this->add($s['value'], $s['p'], $o['value'], $s['type'], $o['type']); + $this->pushS($o); + /* reification */ + if (isset($s['p_id'])) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'] + ); + unset($s['p_id']); + } + $this->state = 2; + } elseif ($a[$this->rdf.'parseType'] === 'Collection') { + $s['o_is_coll'] = true; + $this->state = 4; + } + } else { + /* sub-node or literal */ + $s['o_cdata'] = ''; + if (isset($a[$this->rdf.'datatype'])) { + $s['o_datatype'] = $a[$this->rdf.'datatype']; + } + $this->state = 4; + } + /* any other attrs (skip rdf and xml) */ + foreach ($a as $k => $v) { + if (((strpos($k, $this->xml) === false) && + (strpos($k, $this->rdf) === false)) || + preg_match('/(\_[0-9]+|value)$/', $k)) { + if (strpos($k, ':')) { + if (!$o['value']) { + $o['value'] = $this->graph->newBNodeId(); + $o['type'] = 'bnode'; + $this->add($s['value'], $s['p'], $o['value'], $s['type'], $o['type']); + } + /* reification */ + if (isset($s['p_id'])) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'] + ); + unset($s['p_id']); + } + $this->add($o['value'], $k, $v, $o['type'], 'literal'); + $this->state = 3; + } + } + } + $this->updateS($s); + } + + /** @ignore */ + protected function startState4($t, $a) + { + return $this->startState1($t, $a); + } + + /** @ignore */ + protected function startState5($t, $a) + { + $this->state = 4; + return $this->startState4($t, $a); + } + + /** @ignore */ + protected function startState6($t, $a) + { + $s = $this->getParentS(); + $data = isset($s['o_xml_data']) ? $s['o_xml_data'] : ''; + $ns = isset($s['ns']) ? $s['ns'] : array(); + $parts = $this->splitURI($t); + if (count($parts) === 1) { + $data .= '<'.$t; + } else { + $nsUri = $parts[0]; + $name = $parts[1]; + if (!isset($this->nsp[$nsUri])) { + foreach ($this->nsp as $tmp1 => $tmp2) { + if (strpos($t, $tmp1) === 0) { + $nsUri = $tmp1; + $name = substr($t, strlen($tmp1)); + break; + } + } + } + + $nsp = isset($this->nsp[$nsUri]) ? $this->nsp[$nsUri] : ''; + $data .= $nsp ? '<' . $nsp . ':' . $name : '<' . $name; + /* ns */ + if (!isset($ns[$nsp.'='.$nsUri]) || !$ns[$nsp.'='.$nsUri]) { + $data .= $nsp ? ' xmlns:'.$nsp.'="'.$nsUri.'"' : ' xmlns="'.$nsUri.'"'; + $ns[$nsp.'='.$nsUri] = true; + $s['ns'] = $ns; + } + } + foreach ($a as $k => $v) { + $parts = $this->splitURI($k); + if (count($parts) === 1) { + $data .= ' '.$k.'="'.$v.'"'; + } else { + $nsUri = $parts[0]; + $name = $parts[1]; + $nsp = isset($this->nsp[$nsUri]) ? $this->nsp[$nsUri] : ''; + $data .= $nsp ? ' '.$nsp.':'.$name.'="'.$v.'"' : ' '.$name.'="'.$v.'"' ; + } + } + $data .= '>'; + $s['o_xml_data'] = $data; + $s['o_xml_level'] = isset($s['o_xml_level']) ? $s['o_xml_level'] + 1 : 1; + if ($t == $s['p']) {/* xml container prop */ + $s['p_xml_literal_level'] = isset($s['p_xml_literal_level']) ? $s['p_xml_literal_level'] + 1 : 1; + } + $this->updateS($s); + } + + /** @ignore */ + protected function endState1($t) + { + /* end of doc */ + $this->state = 0; + } + + /** @ignore */ + protected function endState2($t) + { + /* expecting a prop, getting a close */ + if ($s = $this->getParentS()) { + $hasClosingTag = (isset($s['hasClosingTag']) && !$s['hasClosingTag']) ? 0 : 1; + $this->popS(); + $this->state = 5; + if ($s = $this->getParentS()) { + /* new s */ + if (!isset($s['p']) || !$s['p']) { + /* p close after collection|parseType=Resource|node close after p close */ + $this->state = $this->sCount ? 4 : 1; + if (!$hasClosingTag) { + $this->state = 2; + } + } elseif (!$hasClosingTag) { + $this->state = 2; + } + } + } + } + + /** @ignore */ + protected function endState3($t) + { + /* p close */ + $this->state = 2; + } + + /** @ignore */ + protected function endState4($t) + { + /* empty p | pClose after cdata | pClose after collection */ + if ($s = $this->getParentS()) { + $b = isset($s['p_x_base']) && $s['p_x_base'] ? + $s['p_x_base'] : (isset($s['x_base']) ? $s['x_base'] : ''); + if (isset($s['is_coll']) && $s['is_coll']) { + $this->add($s['value'], $this->rdf.'rest', $this->rdf.'nil', $s['type'], 'uri'); + /* back to collection start */ + while ((!isset($s['p']) || ($s['p'] != $t))) { + $subS = $s; + $this->popS(); + $s = $this->getParentS(); + } + /* reification */ + if (isset($s['p_id']) && $s['p_id']) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $subS['value'], + $s['type'], + $subS['type'] + ); + } + unset($s['p']); + $this->updateS($s); + } else { + $dt = isset($s['o_datatype']) ? $s['o_datatype'] : null; + $l = isset($s['p_x_lang']) && $s['p_x_lang'] ? + $s['p_x_lang'] : (isset($s['x_lang']) ? $s['x_lang'] : null); + $o = array('type' => 'literal', 'value' => $s['o_cdata']); + $this->add( + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'], + $dt, + $l + ); + /* reification */ + if (isset($s['p_id']) && $s['p_id']) { + $this->reify( + $b->resolve('#'.$s['p_id']), + $s['value'], + $s['p'], + $o['value'], + $s['type'], + $o['type'], + $dt, + $l + ); + } + unset($s['o_cdata']); + unset($s['o_datatype']); + unset($s['p']); + $this->updateS($s); + } + $this->state = 2; + } + } + + /** @ignore */ + protected function endState5($t) + { + /* p close */ + if ($s = $this->getParentS()) { + unset($s['p']); + $this->updateS($s); + $this->state = 2; + } + } + + /** @ignore */ + protected function endState6($t) + { + if ($s = $this->getParentS()) { + $l = isset($s['p_x_lang']) && $s['p_x_lang'] ? + $s['p_x_lang'] : + (isset($s['x_lang']) ? $s['x_lang'] : null); + $data = $s['o_xml_data']; + $level = $s['o_xml_level']; + if ($level === 0) { + /* pClose */ + $this->add( + $s['value'], + $s['p'], + trim($data, ' '), + $s['type'], + 'literal', + $this->rdf.'XMLLiteral', + $l + ); + unset($s['o_xml_data']); + $this->state = 2; + } else { + $parts = $this->splitURI($t); + if (count($parts) == 1) { + $data .= ''; + } else { + $nsUri = $parts[0]; + $name = $parts[1]; + if (!isset($this->nsp[$nsUri])) { + foreach ($this->nsp as $tmp1 => $tmp2) { + if (strpos($t, $tmp1) === 0) { + $nsUri = $tmp1; + $name = substr($t, strlen($tmp1)); + break; + } + } + } + $nsp = isset($this->nsp[$nsUri]) ? $this->nsp[$nsUri] : ''; + $data .= $nsp ? '' : ''; + } + $s['o_xml_data'] = $data; + $s['o_xml_level'] = $level - 1; + if ($t == $s['p']) { + /* xml container prop */ + $s['p_xml_literal_level']--; + } + } + $this->updateS($s); + } + } + + /** @ignore */ + protected function cdataState4($d) + { + if ($s = $this->getParentS()) { + $s['o_cdata'] = isset($s['o_cdata']) ? $s['o_cdata'] . $d : $d; + $this->updateS($s); + } + } + + /** @ignore */ + protected function cdataState6($d) + { + if ($s = $this->getParentS()) { + if (isset($s['o_xml_data']) || preg_match('/[\n\r]/', $d) || trim($d)) { + $d = htmlspecialchars($d, ENT_NOQUOTES); + $s['o_xml_data'] = isset($s['o_xml_data']) ? $s['o_xml_data'] . $d : $d; + } + $this->updateS($s); + } + } + + /** + * Parse an RDF/XML document into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws Exception + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'rdfxml') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\RdfXml does not support: $format" + ); + } + + $this->init($graph, $baseUri); + $this->resetBnodeMap(); + + /* xml parser */ + $this->initXMLParser(); + + /* parse */ + if (!xml_parse($this->xmlParser, $data, false)) { + $message = xml_error_string(xml_get_error_code($this->xmlParser)); + throw new Exception( + 'XML error: "' . $message . '"', + xml_get_current_line_number($this->xmlParser), + xml_get_current_column_number($this->xmlParser) + ); + } + + xml_parser_free($this->xmlParser); + + return $this->tripleCount; + } +} diff --git a/lib/Parser/Rdfa.php b/lib/Parser/Rdfa.php new file mode 100644 index 0000000..44e447f --- /dev/null +++ b/lib/Parser/Rdfa.php @@ -0,0 +1,728 @@ +debug) { + print "Adding triple: $resource -> $property -> ".$value['type'].':'.$value['value']."\n"; + } + $count = $this->graph->add($resource, $property, $value); + $this->tripleCount += $count; + return $count; + } + + protected function generateList($subject, $property, $list) + { + $current = $subject; + $prop = $property; + + // Output a blank node for each item in the list + foreach ($list as $item) { + $newNode = $this->graph->newBNodeId(); + $this->addTriple($current, $prop, array('type' => 'bnode', 'value' => $newNode)); + $this->addTriple($newNode, 'rdf:first', $item); + + $current = $newNode; + $prop = 'rdf:rest'; + } + + // Finally, terminate the list + $this->addTriple( + $current, + $prop, + array('type' => 'uri', 'value' => RdfNamespace::expand('rdf:nil')) + ); + } + + protected function addToList($listMapping, $property, $value) + { + if ($this->debug) { + print "Adding to list: $property -> ".$value['type'].':'.$value['value']."\n"; + } + + // Create property in the list mapping if it doesn't already exist + if (!isset($listMapping->$property)) { + $listMapping->$property = array(); + } + array_push($listMapping->$property, $value); + } + + protected function printNode($node, $depth) + { + $indent = str_repeat(' ', $depth); + print $indent; + switch ($node->nodeType) { + case XML_ELEMENT_NODE: + print 'node'; + break; + case XML_ATTRIBUTE_NODE: + print 'attr'; + break; + case XML_TEXT_NODE: + print 'text'; + break; + case XML_CDATA_SECTION_NODE: + print 'cdata'; + break; + case XML_ENTITY_REF_NODE: + print 'entref'; + break; + case XML_ENTITY_NODE: + print 'entity'; + break; + case XML_PI_NODE: + print 'pi'; + break; + case XML_COMMENT_NODE: + print 'comment'; + break; + case XML_DOCUMENT_NODE: + print 'doc'; + break; + case XML_DOCUMENT_TYPE_NODE: + print 'doctype'; + break; + case XML_HTML_DOCUMENT_NODE: + print 'html'; + break; + default: + throw new \EasyRdf\Exception("unknown node type: ".$node->nodeType); + break; + } + print ' '.$node->nodeName."\n"; + + if ($node->hasAttributes()) { + foreach ($node->attributes as $attr) { + print $indent.' '.$attr->nodeName." => ".$attr->nodeValue."\n"; + } + } + } + + protected function guessTimeDatatype($value) + { + if (preg_match('/^-?\d{4}-\d{2}-\d{2}(Z|[\-\+]\d{2}:\d{2})?$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#date'; + } elseif (preg_match('/^\d{2}:\d{2}:\d{2}(Z|[\-\+]\d{2}:\d{2})?$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#time'; + } elseif (preg_match('/^-?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[\-\+]\d{2}:\d{2})?$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#dateTime'; + } elseif (preg_match('/^P(\d+Y)?(\d+M)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#duration'; + } elseif (preg_match('/^\d{4}$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#gYear'; + } elseif (preg_match('/^\d{4}-\d{2}$/', $value)) { + return 'http://www.w3.org/2001/XMLSchema#gYearMonth'; + } else { + return null; + } + } + + protected function initialContext() + { + $context = array( + 'prefixes' => array(), + 'vocab' => null, + 'subject' => $this->baseUri, + 'property' => null, + 'object' => null, + 'terms' => array(), + 'incompleteRels' => array(), + 'incompleteRevs' => array(), + 'listMapping' => null, + 'lang' => null, + 'path' => '', + 'xmlns' => array(), + ); + + // Set the default prefix + $context['prefixes'][''] = 'http://www.w3.org/1999/xhtml/vocab#'; + + // RDFa 1.1 default term mapping + $context['terms']['describedby'] = 'http://www.w3.org/2007/05/powder-s#describedby'; + $context['terms']['license'] = 'http://www.w3.org/1999/xhtml/vocab#license'; + $context['terms']['role'] = 'http://www.w3.org/1999/xhtml/vocab#role'; + + return $context; + } + + protected function expandCurie($node, &$context, $value) + { + if (preg_match('/^(\w*?):(.*)$/', $value, $matches)) { + list (, $prefix, $local) = $matches; + $prefix = strtolower($prefix); + if ($prefix === '_') { + // It is a bnode + return $this->remapBnode(substr($value, 2)); + } elseif (empty($prefix) and $context['vocab']) { + // Empty prefix + return $context['vocab'] . $local; + } elseif (isset($context['prefixes'][$prefix])) { + return $context['prefixes'][$prefix] . $local; + } elseif ($uri = $node->lookupNamespaceURI($prefix)) { + return $uri . $local; + } elseif (!empty($prefix) and $uri = RdfNamespace::get($prefix)) { + // Expand using well-known prefixes + return $uri . $local; + } + } + } + + protected function processUri($node, &$context, $value, $isProp = false) + { + if (preg_match('/^\[(.*)\]$/', $value, $matches)) { + // Safe CURIE + return $this->expandCurie($node, $context, $matches[1]); + } elseif (preg_match(self::TERM_REGEXP, $value) and $isProp) { + $term = strtolower($value); + if ($context['vocab']) { + return $context['vocab'] . $value; + } elseif (isset($context['terms'][$term])) { + return $context['terms'][$term]; + } + } elseif (substr($value, 0, 2) === '_:' and $isProp) { + return null; + } else { + $uri = $this->expandCurie($node, $context, $value); + if ($uri) { + return $uri; + } else { + $parsed = new ParsedUri($value); + if ($parsed->isAbsolute()) { + return $value; + } elseif ($isProp) { + // Properties can't be relative URIs + return null; + } elseif ($this->baseUri) { + return $this->baseUri->resolve($parsed); + } + } + } + } + + protected function processUriList($node, $context, $values) + { + if (!$values) { + return array(); + } + + $uris = array(); + foreach (preg_split('/\s+/', $values) as $value) { + $uri = $this->processUri($node, $context, $value, true); + if ($uri) { + array_push($uris, $uri); + } + } + return $uris; + } + + protected function getUriAttribute($node, &$context, $attributes) + { + if (!is_array($attributes)) { + $attributes = array($attributes); + } + + // Find the first attribute that returns a valid URI + foreach ($attributes as $attribute) { + if ($node->hasAttribute($attribute)) { + $value = $node->getAttribute($attribute); + $uri = $this->processUri($node, $context, $value); + if ($uri) { + return $uri; + } + } + } + } + + protected function processNode($node, &$context, $depth = 1) + { + if ($this->debug) { + $this->printNode($node, $depth); + } + + // Step 1: establish local variables + $skip = false; + $subject = null; + $typedResource = null; + $object = null; + $lang = $context['lang']; + $incompleteRels = array(); + $incompleteRevs = array(); + + if ($node->nodeType === XML_ELEMENT_NODE) { + $context['path'] .= '/' . $node->nodeName; + + $content = $node->hasAttribute('content') ? $node->getAttribute('content') : null; + $datatype = $node->hasAttribute('datatype') ? $node->getAttribute('datatype') : null; + $property = $node->getAttribute('property') ? $node->getAttribute('property') : null; + $typeof = $node->getAttribute('typeof') ? $node->getAttribute('typeof') : null; + + // Step 2: Default vocabulary + if ($node->hasAttribute('vocab')) { + $context['vocab'] = $node->getAttribute('vocab'); + if ($context['vocab']) { + $this->addTriple( + $this->baseUri, + 'rdfa:usesVocabulary', + array('type' => 'uri', 'value' => $context['vocab']) + ); + } + } + + // Step 3: Set prefix mappings + // Support for deprecated xmlns if present in document + foreach ($context['xmlns'] as $prefix => $uri) { + if ($node->hasAttribute('xmlns:' . $prefix)) { + $context['prefixes'][$prefix] = $node->getAttribute('xmlns:' . $prefix); + if ($this->debug) { + print "Prefix (xmlns): $prefix => $uri\n"; + } + } + } + if ($node->hasAttribute('prefix')) { + $mappings = preg_split('/\s+/', $node->getAttribute('prefix')); + while (count($mappings)) { + $prefix = strtolower(array_shift($mappings)); + $uri = array_shift($mappings); + + if (substr($prefix, -1) === ':') { + $prefix = substr($prefix, 0, -1); + } else { + continue; + } + + if ($prefix === '_') { + continue; + } elseif (!empty($prefix)) { + $context['prefixes'][$prefix] = $uri; + if ($this->debug) { + print "Prefix: $prefix => $uri\n"; + } + } + } + } + + // Step 4 + if ($node->hasAttributeNS(self::XML_NS, 'lang')) { + $lang = $node->getAttributeNS(self::XML_NS, 'lang'); + } elseif ($node->hasAttribute('lang')) { + $lang = $node->getAttribute('lang'); + } + + // HTML+RDFa 1.1: ignore rel and rev unless they contain CURIEs. + foreach (array('rel', 'rev') as $attr) { + if ($node->hasAttribute('property') and $node->hasAttribute($attr)) { + // Quick check in case there are no CURIEs to deal with. + if (strpos($node->getAttribute($attr), ':') === false) { + $node->removeAttribute($attr); + } else { + // Only keep CURIEs. + $curies = array(); + foreach (preg_split('/\s+/', $node->getAttribute($attr)) as $token) { + if (strpos($token, ':')) { + $curies[] = $token; + } + } + $node->setAttribute($attr, implode(' ', $curies)); + } + } + } + + $rels = $this->processUriList($node, $context, $node->getAttribute('rel')); + $revs = $this->processUriList($node, $context, $node->getAttribute('rev')); + + if (!$node->hasAttribute('rel') and !$node->hasAttribute('rev')) { + // Step 5: Establish a new subject if no rel/rev + if ($property and is_null($content) and is_null($datatype)) { + $subject = $this->getUriAttribute($node, $context, 'about'); + if ($typeof and !$subject) { + $typedResource = $this->getUriAttribute( + $node, + $context, + array('resource', 'href', 'src') + ); + if (!$typedResource) { + $typedResource = $this->graph->newBNodeId(); + } + $object = $typedResource; + } + } else { + $subject = $this->getUriAttribute( + $node, + $context, + array('about', 'resource', 'href', 'src') + ); + } + + // Establish a subject if there isn't one + # FIXME: refactor this + if (is_null($subject)) { + if ($context['path'] === '/html/head') { + $subject = $context['object']; + } elseif ($depth <= 2) { + $subject = $this->baseUri; + } elseif ($typeof and !$property) { + $subject = $this->graph->newBNodeId(); + } else { + if (!$property) { + $skip = true; + } + $subject = $context['object']; + } + } + } else { + // Step 6 + // If the current element does contain a @rel or @rev attribute, then the next step is to + // establish both a value for new subject and a value for current object resource: + + $subject = $this->getUriAttribute($node, $context, 'about'); + + $object = $this->getUriAttribute( + $node, + $context, + array('resource', 'href', 'src') + ); + + if ($typeof) { + if (!$object and !$subject) { + $object = $this->graph->newBNodeId(); + } + $typedResource = $subject ? $subject : $object; + } + + # FIXME: if the element is the root element of the document + # then act as if there is an empty @about present + if (!$subject) { + $subject = $context['object']; + } + } + + # FIXME: better place for this? + if ($typeof and $subject and !$typedResource) { + $typedResource = $subject; + } + + // Step 7: Process @typeof if there is a subject + if ($typedResource) { + foreach ($this->processUriList($node, $context, $typeof) as $type) { + $this->addTriple( + $typedResource, + 'rdf:type', + array('type' => 'uri', 'value' => $type) + ); + } + } + + // Step 8: Create new List mapping if the subject has changed + if ($subject and $subject !== $context['subject']) { + $listMapping = new \stdClass(); + } else { + $listMapping = $context['listMapping']; + } + + // Step 9: Generate triples with given object + if ($subject and $object) { + foreach ($rels as $prop) { + $obj = array('type' => 'uri', 'value' => $object); + if ($node->hasAttribute('inlist')) { + $this->addToList($listMapping, $prop, $obj); + } else { + $this->addTriple($subject, $prop, $obj); + } + } + + foreach ($revs as $prop) { + $this->addTriple( + $object, + $prop, + array('type' => 'uri', 'value' => $subject) + ); + } + } elseif ($rels or $revs) { + // Step 10: Incomplete triples and bnode creation + $object = $this->graph->newBNodeId(); + if ($rels) { + if ($node->hasAttribute('inlist')) { + foreach ($rels as $prop) { + # FIXME: add support for incomplete lists + if (!isset($listMapping->$prop)) { + $listMapping->$prop = array(); + } + } + } else { + $incompleteRels = $rels; + if ($this->debug) { + print "Incomplete rels: ".implode(',', $rels)."\n"; + } + } + } + + if ($revs) { + $incompleteRevs = $revs; + if ($this->debug) { + print "Incomplete revs: ".implode(',', $revs)."\n"; + } + } + } + + // Step 11: establish current property value + if ($subject and $property) { + $value = array(); + + if ($datatype) { + $datatype = $this->processUri($node, $context, $datatype, true); + } + + if ($content !== null) { + $value['value'] = $content; + } elseif ($node->hasAttribute('datetime')) { + $value['value'] = $node->getAttribute('datetime'); + $datetime = true; + } elseif ($datatype === '') { + $value['value'] = $node->textContent; + } elseif ($datatype === self::RDF_XML_LITERAL) { + $value['value'] = ''; + foreach ($node->childNodes as $child) { + $value['value'] .= $child->C14N(); + } + } elseif (is_null($datatype) and empty($rels) and empty($revs)) { + $value['value'] = $this->getUriAttribute( + $node, + $context, + array('resource', 'href', 'src') + ); + + if ($value['value']) { + $value['type'] = 'uri'; + } + } + + if (empty($value['value']) and $typedResource and !$node->hasAttribute('about')) { + $value['type'] = 'uri'; + $value['value'] = $typedResource; + } + + if (empty($value['value'])) { + $value['value'] = $node->textContent; + } + + if (empty($value['type'])) { + $value['type'] = 'literal'; + if ($datatype) { + $value['datatype'] = $datatype; + } elseif (isset($datetime) or $node->nodeName === 'time') { + $value['datatype'] = $this->guessTimeDatatype($value['value']); + } + + if (empty($value['datatype']) and $lang) { + $value['lang'] = $lang; + } + } + + // Add each of the properties + foreach ($this->processUriList($node, $context, $property) as $prop) { + if ($node->hasAttribute('inlist')) { + $this->addToList($listMapping, $prop, $value); + } elseif ($subject) { + $this->addTriple($subject, $prop, $value); + } + } + } + + // Step 12: Complete the incomplete triples from the evaluation context + if (!$skip and $subject and ($context['incompleteRels'] or $context['incompleteRevs'])) { + foreach ($context['incompleteRels'] as $prop) { + $this->addTriple( + $context['subject'], + $prop, + array('type' => 'uri', 'value' => $subject) + ); + } + + foreach ($context['incompleteRevs'] as $prop) { + $this->addTriple( + $subject, + $prop, + array('type' => 'uri', 'value' => $context['subject']) + ); + } + } + } + + // Step 13: create a new evaluation context and proceed recursively + if ($node->hasChildNodes()) { + if ($skip) { + $newContext = $context; + } else { + // Prepare a new evaluation context + $newContext = $context; + if ($object) { + $newContext['object'] = $object; + } elseif ($subject) { + $newContext['object'] = $subject; + } else { + $newContext['object'] = $context['subject']; + } + if ($subject) { + $newContext['subject'] = $subject; + } + $newContext['incompleteRels'] = $incompleteRels; + $newContext['incompleteRevs'] = $incompleteRevs; + if (isset($listMapping)) { + $newContext['listMapping'] = $listMapping; + } + } + + // The language is always updated, even if skip is set + $newContext['lang'] = $lang; + + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_ELEMENT_NODE) { + $this->processNode($child, $newContext, $depth+1); + } + } + } + + // Step 14: create triples for lists + if (!empty($listMapping)) { + foreach ($listMapping as $prop => $list) { + if ($context['listMapping'] !== $listMapping) { + if ($this->debug) { + print "Need to create triples for $prop => ".count($list)." items\n"; + } + $this->generateList($subject, $prop, $list); + } + } + } + } + + /** + * Parse RDFa 1.1 into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param string $data the RDF document data + * @param string $format the format of the input data + * @param string $baseUri the base URI of the data being parsed + * + * @throws \EasyRdf\Exception + * @return integer The number of triples added to the graph + */ + public function parse($graph, $data, $format, $baseUri) + { + parent::checkParseParams($graph, $data, $format, $baseUri); + + if ($format != 'rdfa') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\Rdfa does not support: {$format}" + ); + } + + // Initialise evaluation context. + $context = $this->initialContext(); + + libxml_use_internal_errors(true); + + // Parse the document into DOM + $doc = new \DOMDocument(); + // Attempt to parse the document as strict XML, and fall back to HTML + // if XML parsing fails. + if ($doc->loadXML($data, LIBXML_NONET)) { + if ($this->debug) { + print "Document was parsed as XML."; + } + // Collect all xmlns namespaces defined throughout the document. + $sxe = simplexml_import_dom($doc); + $context['xmlns'] = $sxe->getDocNamespaces(true); + unset($context['xmlns']['']); + } else { + $doc->loadHTML($data); + if ($this->debug) { + print "Document was parsed as HTML."; + } + } + + // Establish the base for both XHTML and HTML documents. + $xpath = new \DOMXPath($doc); + $xpath->registerNamespace('xh', "http://www.w3.org/1999/xhtml"); + $nodeList = $xpath->query('/xh:html/xh:head/xh:base'); + if ($node = $nodeList->item(0) and $href = $node->getAttribute('href')) { + $this->baseUri = new ParsedUri($href); + } + $nodeList = $xpath->query('/html/head/base'); + if ($node = $nodeList->item(0) and $href = $node->getAttribute('href')) { + $this->baseUri = new ParsedUri($href); + } + + // Remove the fragment from the base URI + $this->baseUri->setFragment(null); + + // Recursively process XML nodes + $this->processNode($doc, $context); + + return $this->tripleCount; + } +} diff --git a/lib/Parser/Turtle.php b/lib/Parser/Turtle.php new file mode 100644 index 0000000..d1ea115 --- /dev/null +++ b/lib/Parser/Turtle.php @@ -0,0 +1,1362 @@ +data = $data; + $this->namespaces = array(); + $this->subject = null; + $this->predicate = null; + $this->object = null; + + $this->line = 1; + $this->column = 1; + + $this->bytePos = 0; + $this->dataLength = null; + + $this->resetBnodeMap(); + + $c = $this->skipWSC(); + while ($c != -1) { + $this->parseStatement(); + $c = $this->skipWSC(); + } + + return $this->tripleCount; + } + + + /** + * Parse a statement [2] + * @ignore + */ + protected function parseStatement() + { + $directive = ''; + while (true) { + $c = $this->read(); + if ($c == -1 || self::isWhitespace($c)) { + $this->unread($c); + break; + } else { + $directive .= $c; + } + } + + if (preg_match('/^(@|prefix$|base$)/i', $directive)) { + $this->parseDirective($directive); + $this->skipWSC(); + // SPARQL BASE and PREFIX lines do not end in . + if ($directive[0] == "@") { + $this->verifyCharacterOrFail($this->read(), "."); + } + } else { + $this->unread($directive); + $this->parseTriples(); + $this->skipWSC(); + $this->verifyCharacterOrFail($this->read(), "."); + } + } + + /** + * Parse a directive [3] + * @ignore + */ + protected function parseDirective($directive) + { + $directive = strtolower($directive); + if ($directive == "prefix" || $directive == '@prefix') { + $this->parsePrefixID(); + } elseif ($directive == "base" || $directive == '@base') { + $this->parseBase(); + } elseif (mb_strlen($directive, "UTF-8") == 0) { + throw new Exception( + "Turtle Parse Error: directive name is missing, expected @prefix or @base", + $this->line, + $this->column + ); + } else { + throw new Exception( + "Turtle Parse Error: unknown directive \"$directive\"", + $this->line, + $this->column + ); + } + } + + /** + * Parse a prefixID [4] + * @ignore + */ + protected function parsePrefixID() + { + $this->skipWSC(); + + // Read prefix ID (e.g. "rdf:" or ":") + $prefixID = ''; + + while (true) { + $c = $this->read(); + if ($c == ':') { + $this->unread($c); + break; + } elseif (self::isWhitespace($c)) { + break; + } elseif ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading prefix id", + $this->line, + $this->column + ); + } + + $prefixID .= $c; + } + + $this->skipWSC(); + $this->verifyCharacterOrFail($this->read(), ":"); + $this->skipWSC(); + + // Read the namespace URI + $namespace = $this->parseURI(); + + // Store local namespace mapping + $this->namespaces[$prefixID] = $namespace['value']; + } + + /** + * Parse base [5] + * @ignore + */ + protected function parseBase() + { + $this->skipWSC(); + + $baseUri = $this->parseURI(); + $this->baseUri = new ParsedUri($baseUri['value']); + } + + /** + * Parse triples [6] modified to use a pointer instead of + * manipulating the input buffer directly. + * @ignore + */ + protected function parseTriples() + { + $c = $this->peek(); + + // If the first character is an open bracket we need to decide which of + // the two parsing methods for blank nodes to use + if ($c == '[') { + $c = $this->read(); + $this->skipWSC(); + $c = $this->peek(); + if ($c == ']') { + $c = $this->read(); + $this->subject = $this->createBNode(); + $this->skipWSC(); + $this->parsePredicateObjectList(); + } else { + $this->unskipWS(); + $this->unread('['); + $this->subject = $this->parseImplicitBlank(); + } + $this->skipWSC(); + $c = $this->peek(); + + // if this is not the end of the statement, recurse into the list of + // predicate and objects, using the subject parsed above as the subject + // of the statement. + if ($c != '.') { + $this->parsePredicateObjectList(); + } + } else { + $this->parseSubject(); + $this->skipWSC(); + $this->parsePredicateObjectList(); + } + + $this->subject = null; + $this->predicate = null; + $this->object = null; + } + + /** + * Parse a predicateObjectList [7] + * @ignore + */ + protected function parsePredicateObjectList() + { + $this->predicate = $this->parsePredicate(); + + $this->skipWSC(); + $this->parseObjectList(); + + while ($this->skipWSC() == ';') { + $this->read(); + + $c = $this->skipWSC(); + + if ($c == '.' || $c == ']') { + break; + } elseif ($c == ';') { + // empty predicateObjectList, skip to next + continue; + } + + $this->predicate = $this->parsePredicate(); + + $this->skipWSC(); + + $this->parseObjectList(); + } + } + + /** + * Parse a objectList [8] + * @ignore + */ + protected function parseObjectList() + { + $this->parseObject(); + + while ($this->skipWSC() == ',') { + $this->read(); + $this->skipWSC(); + $this->parseObject(); + } + } + + /** + * Parse a subject [10] + * @ignore + */ + protected function parseSubject() + { + $c = $this->peek(); + if ($c == '(') { + $this->subject = $this->parseCollection(); + } elseif ($c == '[') { + $this->subject = $this->parseImplicitBlank(); + } else { + $value = $this->parseValue(); + + if ($value['type'] == 'uri' or $value['type'] == 'bnode') { + $this->subject = $value; + } else { + throw new Exception( + "Turtle Parse Error: illegal subject type: ".$value['type'], + $this->line, + $this->column + ); + } + } + } + + /** + * Parse a predicate [11] + * @ignore + */ + protected function parsePredicate() + { + // Check if the short-cut 'a' is used + $c1 = $this->read(); + + if ($c1 == 'a') { + $c2 = $this->read(); + + if (self::isWhitespace($c2)) { + // Short-cut is used, return the rdf:type URI + return array( + 'type' => 'uri', + 'value' => RdfNamespace::get('rdf') . 'type' + ); + } + + // Short-cut is not used, unread all characters + $this->unread($c2); + } + $this->unread($c1); + + // Predicate is a normal resource + $predicate = $this->parseValue(); + if ($predicate['type'] == 'uri') { + return $predicate; + } else { + throw new Exception( + "Turtle Parse Error: Illegal predicate type: " . $predicate['type'], + $this->line, + $this->column + ); + } + } + + /** + * Parse a object [12] + * @ignore + */ + protected function parseObject() + { + $c = $this->peek(); + + if ($c == '(') { + $this->object = $this->parseCollection(); + } elseif ($c == '[') { + $this->object = $this->parseImplicitBlank(); + } else { + $this->object = $this->parseValue(); + } + + $this->addTriple( + $this->subject['value'], + $this->predicate['value'], + $this->object + ); + } + + /** + * Parses a blankNodePropertyList [15] + * + * This method parses the token [] + * and predicateObjectLists that are surrounded by square brackets. + * + * @ignore + */ + protected function parseImplicitBlank() + { + $this->verifyCharacterOrFail($this->read(), "["); + + $bnode = $this->createBNode(); + + $c = $this->read(); + if ($c != ']') { + $this->unread($c); + + // Remember current subject and predicate + $oldSubject = $this->subject; + $oldPredicate = $this->predicate; + + // generated bNode becomes subject + $this->subject = $bnode; + + // Enter recursion with nested predicate-object list + $this->skipWSC(); + + $this->parsePredicateObjectList(); + + $this->skipWSC(); + + // Read closing bracket + $this->verifyCharacterOrFail($this->read(), "]"); + + // Restore previous subject and predicate + $this->subject = $oldSubject; + $this->predicate = $oldPredicate; + } + + return $bnode; + } + + /** + * Parses a collection [16], e.g: ( item1 item2 item3 ) + * @ignore + */ + protected function parseCollection() + { + $this->verifyCharacterOrFail($this->read(), "("); + + $c = $this->skipWSC(); + if ($c == ')') { + // Empty list + $this->read(); + return array( + 'type' => 'uri', + 'value' => RdfNamespace::get('rdf') . 'nil' + ); + } else { + $listRoot = $this->createBNode(); + + // Remember current subject and predicate + $oldSubject = $this->subject; + $oldPredicate = $this->predicate; + + // generated bNode becomes subject, predicate becomes rdf:first + $this->subject = $listRoot; + $this->predicate = array( + 'type' => 'uri', + 'value' => RdfNamespace::get('rdf') . 'first' + ); + + $this->parseObject(); + $bNode = $listRoot; + + while ($this->skipWSC() != ')') { + // Create another list node and link it to the previous + $newNode = $this->createBNode(); + + $this->addTriple( + $bNode['value'], + RdfNamespace::get('rdf') . 'rest', + $newNode + ); + + // New node becomes the current + $this->subject = $bNode = $newNode; + + $this->parseObject(); + } + + // Skip ')' + $this->read(); + + // Close the list + $this->addTriple( + $bNode['value'], + RdfNamespace::get('rdf') . 'rest', + array( + 'type' => 'uri', + 'value' => RdfNamespace::get('rdf') . 'nil' + ) + ); + + // Restore previous subject and predicate + $this->subject = $oldSubject; + $this->predicate = $oldPredicate; + + return $listRoot; + } + } + + /** + * Parses an RDF value. This method parses uriref, qname, node ID, quoted + * literal, integer, double and boolean. + * @ignore + */ + protected function parseValue() + { + $c = $this->peek(); + + if ($c == '<') { + // uriref, e.g. + return $this->parseURI(); + } elseif ($c == ':' || self::isPrefixStartChar($c)) { + // qname or boolean + return $this->parseQNameOrBoolean(); + } elseif ($c == '_') { + // node ID, e.g. _:n1 + return $this->parseNodeID(); + } elseif ($c == '"' || $c == "'") { + // quoted literal, e.g. "foo" or """foo""" or 'foo' or '''foo''' + return $this->parseQuotedLiteral(); + } elseif (ctype_digit($c) || $c == '.' || $c == '+' || $c == '-') { + // integer or double, e.g. 123 or 1.2e3 + return $this->parseNumber(); + } elseif ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading value", + $this->line, + $this->column + ); + } else { + throw new Exception( + "Turtle Parse Error: expected an RDF value here, found '$c'", + $this->line, + $this->column + ); + } + } + + /** + * Parses a quoted string, optionally followed by a language tag or datatype. + * @ignore + */ + protected function parseQuotedLiteral() + { + $label = $this->parseQuotedString(); + + // Check for presence of a language tag or datatype + $c = $this->peek(); + + if ($c == '@') { + $this->read(); + + // Read language + $lang = ''; + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading language", + $this->line, + $this->column + ); + } elseif (!self::isLanguageStartChar($c)) { + throw new Exception( + "Turtle Parse Error: expected a letter, found '$c'", + $this->line, + $this->column + ); + } + + $lang .= $c; + + $c = $this->read(); + while (!self::isWhitespace($c)) { + if ($c == '.' || $c == ';' || $c == ',' || $c == ')' || $c == ']' || $c == -1) { + break; + } + if (self::isLanguageChar($c)) { + $lang .= $c; + } else { + throw new Exception( + "Turtle Parse Error: illegal language tag char: '$c'", + $this->line, + $this->column + ); + } + $c = $this->read(); + } + + $this->unread($c); + + return array( + 'type' => 'literal', + 'value' => $label, + 'lang' => $lang + ); + } elseif ($c == '^') { + $this->read(); + + // next character should be another '^' + $this->verifyCharacterOrFail($this->read(), "^"); + + // Read datatype + $datatype = $this->parseValue(); + if ($datatype['type'] == 'uri') { + return array( + 'type' => 'literal', + 'value' => $label, + 'datatype' => $datatype['value'] + ); + } else { + throw new Exception( + "Turtle Parse Error: illegal datatype type: " . $datatype['type'], + $this->line, + $this->column + ); + } + } else { + return array( + 'type' => 'literal', + 'value' => $label + ); + } + } + + /** + * Parses a quoted string, which is either a "normal string" or a """long string""". + * @ignore + */ + protected function parseQuotedString() + { + $result = null; + + $c1 = $this->read(); + + // First character should be ' or " + $this->verifyCharacterOrFail($c1, "\"\'"); + + // Check for long-string, which starts and ends with three double quotes + $c2 = $this->read(); + $c3 = $this->read(); + + if ($c2 == $c1 && $c3 == $c1) { + // Long string + $result = $this->parseLongString($c2); + } else { + // Normal string + $this->unread($c3); + $this->unread($c2); + + $result = $this->parseString($c1); + } + + // Unescape any escape sequences + return $this->unescapeString($result); + } + + /** + * Parses a "normal string". This method requires that the opening character + * has already been parsed. + * + * @param string $closingCharacter The type of quote to use (either ' or ") + * + * @throws Exception + * @return string + * @ignore + */ + protected function parseString($closingCharacter) + { + $str = ''; + + while (true) { + $c = $this->read(); + + if ($c == $closingCharacter) { + break; + } elseif ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading string", + $this->line, + $this->column + ); + } + + $str .= $c; + + if ($c == '\\') { + // This escapes the next character, which might be a ' or a " + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading string", + $this->line, + $this->column + ); + } + $str .= $c; + } + } + + return $str; + } + + /** + * Parses a """long string""". This method requires that the first three + * characters have already been parsed. + * + * @param string $closingCharacter The type of quote to use (either ' or ") + * + * @throws Exception + * @return string + * @ignore + */ + protected function parseLongString($closingCharacter) + { + $str = ''; + $doubleQuoteCount = 0; + + while ($doubleQuoteCount < 3) { + $c = $this->read(); + + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading long string", + $this->line, + $this->column + ); + } elseif ($c == $closingCharacter) { + $doubleQuoteCount++; + } else { + $doubleQuoteCount = 0; + } + + $str .= $c; + + if ($c == '\\') { + // This escapes the next character, which might be a ' or " + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading long string", + $this->line, + $this->column + ); + } + $str .= $c; + } + } + + return mb_substr($str, 0, -3, "UTF-8"); + } + + /** + * Parses a numeric value, either of type integer, decimal or double + * @ignore + */ + protected function parseNumber() + { + $value = ''; + $datatype = RdfNamespace::get('xsd').'integer'; + + $c = $this->read(); + + // read optional sign character + if ($c == '+' || $c == '-') { + $value .= $c; + $c = $this->read(); + } + + while (ctype_digit($c)) { + $value .= $c; + $c = $this->read(); + } + + if ($c == '.' || $c == 'e' || $c == 'E') { + // read optional fractional digits + if ($c == '.') { + if (self::isWhitespace($this->peek())) { + // We're parsing an integer that did not have a space before the + // period to end the statement + } else { + $value .= $c; + $c = $this->read(); + while (ctype_digit($c)) { + $value .= $c; + $c = $this->read(); + } + + if (mb_strlen($value, "UTF-8") == 1) { + // We've only parsed a '.' + throw new Exception( + "Turtle Parse Error: object for statement missing", + $this->line, + $this->column + ); + } + + // We're parsing a decimal or a double + $datatype = RdfNamespace::get('xsd').'decimal'; + } + } else { + if (mb_strlen($value, "UTF-8") == 0) { + // We've only parsed an 'e' or 'E' + throw new Exception( + "Turtle Parse Error: object for statement missing", + $this->line, + $this->column + ); + } + } + + // read optional exponent + if ($c == 'e' || $c == 'E') { + $datatype = RdfNamespace::get('xsd').'double'; + $value .= $c; + + $c = $this->read(); + if ($c == '+' || $c == '-') { + $value .= $c; + $c = $this->read(); + } + + if (!ctype_digit($c)) { + throw new Exception( + "Turtle Parse Error: exponent value missing", + $this->line, + $this->column + ); + } + + $value .= $c; + + $c = $this->read(); + while (ctype_digit($c)) { + $value .= $c; + $c = $this->read(); + } + } + } + + // Unread last character, it isn't part of the number + $this->unread($c); + + // Return result as a typed literal + return array( + 'type' => 'literal', + 'value' => $value, + 'datatype' => $datatype + ); + } + + /** + * Parses a URI / IRI + * @ignore + */ + protected function parseURI() + { + $uri = ''; + + // First character should be '<' + $this->verifyCharacterOrFail($this->read(), "<"); + + // Read up to the next '>' character + while (true) { + $c = $this->read(); + + if ($c == '>') { + break; + } elseif ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading URI", + $this->line, + $this->column + ); + } + + $uri .= $c; + + if ($c == '\\') { + // This escapes the next character, which might be a '>' + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading URI", + $this->line, + $this->column + ); + } + $uri .= $c; + } + } + + // Unescape any escape sequences + $uri = $this->unescapeString($uri); + + return array( + 'type' => 'uri', + 'value' => $this->resolve($uri) + ); + } + + /** + * Parses qnames and boolean values, which have equivalent starting + * characters. + * @ignore + */ + protected function parseQNameOrBoolean() + { + // First character should be a ':' or a letter + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while readying value", + $this->line, + $this->column + ); + } + if ($c != ':' && !self::isPrefixStartChar($c)) { + throw new Exception( + "Turtle Parse Error: expected a ':' or a letter, found '$c'", + $this->line, + $this->column + ); + } + + $namespace = null; + + if ($c == ':') { + // qname using default namespace + if (isset($this->namespaces[''])) { + $namespace = $this->namespaces['']; + } else { + throw new Exception( + "Turtle Parse Error: default namespace used but not defined", + $this->line, + $this->column + ); + } + } else { + // $c is the first letter of the prefix + $prefix = $c; + + $c = $this->read(); + while (self::isPrefixChar($c)) { + $prefix .= $c; + $c = $this->read(); + } + + if ($c != ':') { + // prefix may actually be a boolean value + $value = $prefix; + + if ($value == "true" || $value == "false") { + return array( + 'type' => 'literal', + 'value' => $value, + 'datatype' => RdfNamespace::get('xsd') . 'boolean' + ); + } + } + + $this->verifyCharacterOrFail($c, ":"); + + if (isset($this->namespaces[$prefix])) { + $namespace = $this->namespaces[$prefix]; + } else { + throw new Exception( + "Turtle Parse Error: namespace prefix '$prefix' used but not defined", + $this->line, + $this->column + ); + } + } + + // $c == ':', read optional local name + $localName = ''; + $c = $this->read(); + if (self::isNameStartChar($c)) { + if ($c == '\\') { + $localName .= $this->readLocalEscapedChar(); + } else { + $localName .= $c; + } + + $c = $this->read(); + while (self::isNameChar($c)) { + if ($c == '\\') { + $localName .= $this->readLocalEscapedChar(); + } else { + $localName .= $c; + } + $c = $this->read(); + } + } + + // Unread last character + $this->unread($c); + + // Note: namespace has already been resolved + return array( + 'type' => 'uri', + 'value' => $namespace . $localName + ); + } + + protected function readLocalEscapedChar() + { + $c = $this->read(); + + if (self::isLocalEscapedChar($c)) { + return $c; + } else { + throw new Exception( + "found '" . $c . "', expected one of: " . implode(', ', self::$localEscapedChars), + $this->line, + $this->column + ); + } + } + + /** + * Parses a blank node ID, e.g: _:node1 + * @ignore + */ + protected function parseNodeID() + { + // Node ID should start with "_:" + $this->verifyCharacterOrFail($this->read(), "_"); + $this->verifyCharacterOrFail($this->read(), ":"); + + // Read the node ID + $c = $this->read(); + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file while reading node id", + $this->line, + $this->column + ); + } elseif (!self::isNameStartChar($c)) { + throw new Exception( + "Turtle Parse Error: expected a letter, found '$c'", + $this->line, + $this->column + ); + } + + // Read all following letter and numbers, they are part of the name + $name = $c; + $c = $this->read(); + while (self::isNameChar($c)) { + $name .= $c; + $c = $this->read(); + } + + $this->unread($c); + + return array( + 'type' => 'bnode', + 'value' => $this->remapBnode($name) + ); + } + + protected function resolve($uri) + { + if ($this->baseUri) { + return $this->baseUri->resolve($uri)->toString(); + } else { + return $uri; + } + } + + /** + * Verifies that the supplied character $c is one of the expected + * characters specified in $expected. This method will throw a + * exception if this is not the case. + * @ignore + */ + protected function verifyCharacterOrFail($c, $expected) + { + if ($c == -1) { + throw new Exception( + "Turtle Parse Error: unexpected end of file", + $this->line, + $this->column + ); + } elseif (strpbrk($c, $expected) === false) { + $msg = 'expected '; + for ($i = 0; $i < strlen($expected); $i++) { + if ($i > 0) { + $msg .= " or "; + } + $msg .= '\''.$expected[$i].'\''; + } + $msg .= ", found '$c'"; + + throw new Exception( + "Turtle Parse Error: $msg", + $this->line, + $this->column + ); + } + } + + /** + * Skip through whitespace and comments + * @ignore + */ + protected function skipWSC() + { + $c = $this->read(); + while (self::isWhitespace($c) || $c == '#') { + if ($c == '#') { + $this->processComment(); + } + + $c = $this->read(); + } + + $this->unread($c); + return $c; + } + + /** + * Consumes characters from reader until the first EOL has been read. + * @ignore + */ + protected function processComment() + { + $comment = ''; + $c = $this->read(); + while ($c != -1 && $c != "\r" && $c != "\n") { + $comment .= $c; + $c = $this->read(); + } + + // c is equal to -1, \r or \n. + // In case c is equal to \r, we should also read a following \n. + if ($c == "\r") { + $c = $this->read(); + if ($c != "\n") { + $this->unread($c); + } + } + } + + /** + * Read a single character from the input buffer. + * Returns -1 when the end of the file is reached. + * Does not manipulate the data variable. Keeps track of the + * byte position instead. + * @ignore + */ + protected function read() + { + $char = $this->peek(); + if ($char == -1) { + return -1; + } + $this->bytePos += strlen($char); + // Keep tracks of which line we are on (0A = Line Feed) + if ($char == "\x0A") { + $this->line += 1; + $this->column = 1; + } else { + $this->column += 1; + } + return $char; + } + + /** + * Gets the next character to be returned by read() + * without moving the pointer position. Speeds up the + * mb_substr() call by only giving it the next 4 bytes to parse. + * @ignore + */ + protected function peek() + { + if (!$this->dataLength) { + $this->dataLength = strlen($this->data); + } + if ($this->dataLength > $this->bytePos) { + $slice = substr($this->data, $this->bytePos, 4); + return mb_substr($slice, 0, 1, "UTF-8"); + } else { + return -1; + } + } + + + /** + * Steps back, restoring the previous character read() to the input buffer + */ + protected function unread($chars) + { + $this->column -= mb_strlen($chars, "UTF-8"); + $this->bytePos -= strlen($chars); + if ($this->bytePos < 0) { + $this->bytePos = 0; + } + if ($this->column < 1) { + $this->column = 1; + } + } + + /** + * Reverse skips through whitespace in 4 byte increments. + * (Keeps the byte pointer accurate when unreading.) + * @ignore + */ + protected function unskipWS() + { + if ($this->bytePos - 4 > 0) { + $slice = substr($this->data, $this->bytePos - 4, 4); + while ($slice != '') { + if (!self::isWhitespace(mb_substr($slice, -1, 1, "UTF-8"))) { + return; + } + $slice = substr($slice, 0, -1); + $this->bytePos -= 1; + } + // This 4 byte slice was full of whitespace. + // We need to check that there isn't more in the next slice. + $this->unSkipWS(); + } + } + + /** @ignore */ + protected function createBNode() + { + return array( + 'type' => 'bnode', + 'value' => $this->graph->newBNodeId() + ); + } + + /** + * Returns true if $c is a whitespace character + * @ignore + */ + public static function isWhitespace($c) + { + // Whitespace character are space, tab, newline and carriage return: + return $c == "\x20" || $c == "\x09" || $c == "\x0A" || $c == "\x0D"; + } + + /** @ignore */ + public static function isPrefixStartChar($c) + { + $o = ord($c); + return + $o >= 0x41 && $o <= 0x5a || # A-Z + $o >= 0x61 && $o <= 0x7a || # a-z + $o >= 0x00C0 && $o <= 0x00D6 || + $o >= 0x00D8 && $o <= 0x00F6 || + $o >= 0x00F8 && $o <= 0x02FF || + $o >= 0x0370 && $o <= 0x037D || + $o >= 0x037F && $o <= 0x1FFF || + $o >= 0x200C && $o <= 0x200D || + $o >= 0x2070 && $o <= 0x218F || + $o >= 0x2C00 && $o <= 0x2FEF || + $o >= 0x3001 && $o <= 0xD7FF || + $o >= 0xF900 && $o <= 0xFDCF || + $o >= 0xFDF0 && $o <= 0xFFFD || + $o >= 0x10000 && $o <= 0xEFFFF; + } + + /** @ignore */ + public static function isNameStartChar($c) + { + return + $c == '\\' || + $c == '_' || + $c == ':' || + $c == '%' || + ctype_digit($c) || + self::isPrefixStartChar($c); + } + + /** @ignore */ + public static function isNameChar($c) + { + $o = ord($c); + return + self::isNameStartChar($c) || + $o >= 0x30 && $o <= 0x39 || # 0-9 + $c == '-' || + $o == 0x00B7 || + $o >= 0x0300 && $o <= 0x036F || + $o >= 0x203F && $o <= 0x2040; + } + + /** @ignore */ + private static $localEscapedChars = array( + '_', '~', '.', '-', '!', '$', '&', '\'', '(', ')', + '*', '+', ',', ';', '=', '/', '?', '#', '@', '%' + ); + + /** @ignore */ + public static function isLocalEscapedChar($c) + { + return in_array($c, self::$localEscapedChars); + } + + /** @ignore */ + public static function isPrefixChar($c) + { + $o = ord($c); + return + $c == '_' || + $o >= 0x30 && $o <= 0x39 || # 0-9 + self::isPrefixStartChar($c) || + $c == '-' || + $o == 0x00B7 || + $c >= 0x0300 && $c <= 0x036F || + $c >= 0x203F && $c <= 0x2040; + } + + /** @ignore */ + public static function isLanguageStartChar($c) + { + $o = ord($c); + return + $o >= 0x41 && $o <= 0x5a || # A-Z + $o >= 0x61 && $o <= 0x7a; # a-z + } + + /** @ignore */ + public static function isLanguageChar($c) + { + $o = ord($c); + return + $o >= 0x41 && $o <= 0x5a || # A-Z + $o >= 0x61 && $o <= 0x7a || # a-z + $o >= 0x30 && $o <= 0x39 || # 0-9 + $c == '-'; + } +} diff --git a/lib/RdfNamespace.php b/lib/RdfNamespace.php new file mode 100644 index 0000000..9cc2058 --- /dev/null +++ b/lib/RdfNamespace.php @@ -0,0 +1,443 @@ + 'https://www.w3.org/ns/activitystreams#', + 'bibo' => 'http://purl.org/ontology/bibo/', + 'cc' => 'http://creativecommons.org/ns#', + 'cert' => 'http://www.w3.org/ns/auth/cert#', + 'csvw' => 'http://www.w3.org/ns/csvw#', + 'ctag' => 'http://commontag.org/ns#', + 'dc' => 'http://purl.org/dc/terms/', + 'dc11' => 'http://purl.org/dc/elements/1.1/', + 'dcat' => 'http://www.w3.org/ns/dcat#', + 'dcterms' => 'http://purl.org/dc/terms/', + 'doap' => 'http://usefulinc.com/ns/doap#', + 'dqv' => 'http://www.w3.org/ns/dqv#', + 'duv' => 'https://www.w3.org/ns/duv#', + 'exif' => 'http://www.w3.org/2003/12/exif/ns#', + 'foaf' => 'http://xmlns.com/foaf/0.1/', + 'geo' => 'http://www.w3.org/2003/01/geo/wgs84_pos#', + 'gr' => 'http://purl.org/goodrelations/v1#', + 'grddl' => 'http://www.w3.org/2003/g/data-view#', + 'ical' => 'http://www.w3.org/2002/12/cal/icaltzd#', + 'jsonld' => 'http://www.w3.org/ns/json-ld#', + 'ldp' => 'http://www.w3.org/ns/ldp#', + 'ma' => 'http://www.w3.org/ns/ma-ont#', + 'oa' => 'http://www.w3.org/ns/oa#', + 'odrl' => 'http://www.w3.org/ns/odrl/2/', + 'og' => 'http://ogp.me/ns#', + 'org' => 'http://www.w3.org/ns/org#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'prov' => 'http://www.w3.org/ns/prov#', + 'qb' => 'http://purl.org/linked-data/cube#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfa' => 'http://www.w3.org/ns/rdfa#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'rev' => 'http://purl.org/stuff/rev#', + 'rif' => 'http://www.w3.org/2007/rif#', + 'rr' => 'http://www.w3.org/ns/r2rml#', + 'rss' => 'http://purl.org/rss/1.0/', + 'schema' => 'http://schema.org/', + 'sd' => 'http://www.w3.org/ns/sparql-service-description#', + 'sioc' => 'http://rdfs.org/sioc/ns#', + 'skos' => 'http://www.w3.org/2004/02/skos/core#', + 'skosxl' => 'http://www.w3.org/2008/05/skos-xl#', + 'sosa' => 'http://www.w3.org/ns/sosa/', + 'ssn' => 'http://www.w3.org/ns/ssn/', + 'synd' => 'http://purl.org/rss/1.0/modules/syndication/', + 'time' => 'http://www.w3.org/2006/time#', + 'v' => 'http://rdf.data-vocabulary.org/#', + 'vcard' => 'http://www.w3.org/2006/vcard/ns#', + 'void' => 'http://rdfs.org/ns/void#', + 'wdr' => 'http://www.w3.org/2007/05/powder#', + 'wdrs' => 'http://www.w3.org/2007/05/powder-s#', + 'wot' => 'http://xmlns.com/wot/0.1/', + 'xhv' => 'http://www.w3.org/1999/xhtml/vocab#', + 'xml' => 'http://www.w3.org/XML/1998/namespace', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', + ); + + private static $namespaces = null; + + private static $default = null; + + /** Counter for numbering anonymous namespaces */ + private static $anonymousNamespaceCount = 0; + + /** + * Return all the namespaces registered + * + * @return array Associative array of all the namespaces. + */ + public static function namespaces() + { + if (self::$namespaces === null) { + self::resetNamespaces(); + } + + return self::$namespaces; + } + + /** + * Resets list of namespaces to the one, which is provided by EasyRDF + * useful for tests, among other things + */ + public static function resetNamespaces() + { + self::$namespaces = self::$initial_namespaces; + } + + /** + * Return a namespace given its prefix. + * + * @param string $prefix The namespace prefix (eg 'foaf') + * + * @throws \InvalidArgumentException + * @return string The namespace URI (eg 'http://xmlns.com/foaf/0.1/') + */ + public static function get($prefix) + { + if (!is_string($prefix) or $prefix === null) { + throw new \InvalidArgumentException( + "\$prefix should be a string and cannot be null or empty" + ); + } + + if (preg_match('/\W/', $prefix)) { + throw new \InvalidArgumentException( + "\$prefix should only contain alpha-numeric characters" + ); + } + + $prefix = strtolower($prefix); + $namespaces = self::namespaces(); + + if (array_key_exists($prefix, $namespaces)) { + return $namespaces[$prefix]; + } else { + return null; + } + } + + /** + * Register a new namespace. + * + * @param string $prefix The namespace prefix (eg 'foaf') + * @param string $long The namespace URI (eg 'http://xmlns.com/foaf/0.1/') + * + * @throws \LogicException + * @throws \InvalidArgumentException + */ + public static function set($prefix, $long) + { + if (!is_string($prefix) or $prefix === null) { + throw new \InvalidArgumentException( + "\$prefix should be a string and cannot be null or empty" + ); + } + + if ($prefix !== '') { + // prefix ::= Name minus ":" // see: http://www.w3.org/TR/REC-xml-names/#NT-NCName + // Name ::= NameStartChar (NameChar)* // see: http://www.w3.org/TR/REC-xml/#NT-Name + // NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | + // [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | + // [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + // NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] + + $_name_start_char = + 'A-Z_a-z\xc0-\xD6\xd8-\xf6\xf8-\xff\x{0100}-\x{02ff}\x{0370}-\x{037d}' . + '\x{037F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}' . + '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}'; + + $_name_char = + $_name_start_char . + '\-.0-9\xb7\x{0300}-\x{036f}\x{203f}-\x{2040}'; + + $regex = "#^[{$_name_start_char}]{1}[{$_name_char}]{0,}$#u"; + + $match_result = preg_match($regex, $prefix); + + if ($match_result === false) { + throw new \LogicException('regexp error'); + } + + if ($match_result === 0) { + throw new \InvalidArgumentException( + "\$prefix should match RDFXML-QName specification. got: {$prefix}" + ); + } + } + + if (!is_string($long) or $long === null or $long === '') { + throw new \InvalidArgumentException( + "\$long should be a string and cannot be null or empty" + ); + } + + $prefix = strtolower($prefix); + + $namespaces = self::namespaces(); + $namespaces[$prefix] = $long; + + self::$namespaces = $namespaces; + } + + /** + * Get the default namespace + * + * Returns the URI of the default namespace or null + * if no default namespace is defined. + * + * @return string The URI of the default namespace + */ + public static function getDefault() + { + return self::$default; + } + + /** + * Set the default namespace + * + * Set the default namespace to either a URI or the prefix of + * an already defined namespace. + * + * Example: + * EasyRdf\RdfNamespace::setDefault('http://schema.org/'); + * + * @param string $namespace The URI or prefix of a namespace (eg 'og') + * + * @throws \InvalidArgumentException + */ + public static function setDefault($namespace) + { + if (is_null($namespace) or $namespace === '') { + self::$default = null; + } elseif (preg_match('/^\w+$/', $namespace)) { + $namespaces = self::namespaces(); + + if (!isset($namespaces[$namespace])) { + throw new \InvalidArgumentException( + "Unable to set default namespace to unknown prefix: $namespace" + ); + } + + self::$default = $namespaces[$namespace]; + } else { + self::$default = $namespace; + } + } + + /** + * Delete an existing namespace. + * + * @param string $prefix The namespace prefix (eg 'foaf') + * + * @throws \InvalidArgumentException + */ + public static function delete($prefix) + { + if (!is_string($prefix) or $prefix === null or $prefix === '') { + throw new \InvalidArgumentException( + "\$prefix should be a string and cannot be null or empty" + ); + } + + $prefix = strtolower($prefix); + self::namespaces(); // make sure, that self::$namespaces is initialized + if (isset(self::$namespaces[$prefix])) { + unset(self::$namespaces[$prefix]); + } + } + + /** + * Delete the anonymous namespaces and reset the counter to 0 + */ + public static function reset() + { + while (self::$anonymousNamespaceCount > 0) { + self::delete('ns'.(self::$anonymousNamespaceCount-1)); + self::$anonymousNamespaceCount--; + } + } + + /** + * Try and breakup a URI into a prefix and local part + * + * If $createNamespace is true, and the URI isn't part of an existing + * namespace, then EasyRdf will attempt to create a new namespace and + * return the name of the new prefix (for example 'ns0', 'term'). + * + * If it isn't possible to split the URI, then null will be returned. + * + * @param string $uri The full URI (eg 'http://xmlns.com/foaf/0.1/name') + * @param bool $createNamespace If true, a new namespace will be created + * + * @throws \InvalidArgumentException + * @return array The split URI (eg 'foaf', 'name') or null + */ + public static function splitUri($uri, $createNamespace = false) + { + if ($uri === null or $uri === '') { + throw new \InvalidArgumentException( + "\$uri cannot be null or empty" + ); + } + + if (is_object($uri) and ($uri instanceof Resource)) { + $uri = $uri->getUri(); + } elseif (!is_string($uri)) { + throw new \InvalidArgumentException( + '$uri should be a string or EasyRdf\Resource' + ); + } + + foreach (self::namespaces() as $prefix => $long) { + if (substr($uri, 0, strlen($long)) !== $long) { + continue; + } + + $local_part = substr($uri, strlen($long)); + + if (strpos($local_part, '/') !== false) { + // we can't have '/' in local part + continue; + } + + return array($prefix, $local_part); + } + + if ($createNamespace) { + // Try and create a new namespace + # FIXME: check the valid characters for an XML element name + if (preg_match('/^(.+?)([\w\-]+)$/', $uri, $matches)) { + $prefix = "ns".(self::$anonymousNamespaceCount++); + self::set($prefix, $matches[1]); + return array($prefix, $matches[2]); + } + } + + return null; + } + + /** + * Return the prefix namespace that a URI belongs to. + * + * @param string $uri A full URI (eg 'http://xmlns.com/foaf/0.1/name') + * + * @return string The prefix namespace that it is a part of(eg 'foaf') + */ + public static function prefixOfUri($uri) + { + if ($parts = self::splitUri($uri)) { + return $parts[0]; + } + } + + /** + * Shorten a URI by substituting in the namespace prefix. + * + * If $createNamespace is true, and the URI isn't part of an existing + * namespace, then EasyRdf will attempt to create a new namespace and + * use that namespace to shorten the URI (for example ns0:term). + * + * If it isn't possible to shorten the URI, then null will be returned. + * + * @param string $uri The full URI (eg 'http://xmlns.com/foaf/0.1/name') + * @param bool $createNamespace If true, a new namespace will be created + * + * @return string The shortened URI (eg 'foaf:name') or null + */ + public static function shorten($uri, $createNamespace = false) + { + if ($parts = self::splitUri($uri, $createNamespace)) { + return implode(':', $parts); + } + } + + /** + * Expand a shortened URI (qname) back into a full URI. + * + * If it isn't possible to expand the qname, for example if the namespace + * isn't registered, then the original string will be returned. + * + * @param string $shortUri The short URI (eg 'foaf:name') + * + * @throws \InvalidArgumentException + * @return string The full URI (eg 'http://xmlns.com/foaf/0.1/name') + */ + public static function expand($shortUri) + { + if (!is_string($shortUri) or $shortUri === '') { + throw new \InvalidArgumentException( + "\$shortUri should be a string and cannot be null or empty" + ); + } + + if ($shortUri === 'a') { + $namespaces = self::namespaces(); + return $namespaces['rdf'] . 'type'; + } elseif (preg_match('/^(\w+?):([\w\-]+)$/', $shortUri, $matches)) { + $long = self::get($matches[1]); + if ($long) { + return $long . $matches[2]; + } + } elseif (preg_match('/^(\w+)$/', $shortUri) and isset(self::$default)) { + return self::$default . $shortUri; + } + + return $shortUri; + } +} diff --git a/lib/Resource.php b/lib/Resource.php new file mode 100644 index 0000000..044dd14 --- /dev/null +++ b/lib/Resource.php @@ -0,0 +1,828 @@ +resource('http://www.example.com/'); + * + */ + public function __construct($uri, $graph = null) + { + if (!is_string($uri) or $uri == null or $uri == '') { + throw new \InvalidArgumentException( + "\$uri should be a string and cannot be null or empty" + ); + } + + $this->uri = $uri; + + // Check that $graph is an EasyRdf\Graph object + if (is_object($graph) and $graph instanceof Graph) { + $this->graph = $graph; + } elseif (!is_null($graph)) { + throw new \InvalidArgumentException( + '$graph should be an EasyRdf\Graph object' + ); + } + } + + /** + * Return the graph that this resource belongs to + * + * @return Graph + */ + public function getGraph() + { + return $this->graph; + } + + /** Returns the URI for the resource. + * + * @return string URI of this resource. + */ + public function getUri() + { + return $this->uri; + } + + /** Check to see if a resource is a blank node. + * + * @return bool True if this resource is a blank node. + */ + public function isBNode() + { + if (substr($this->uri, 0, 2) == '_:') { + return true; + } else { + return false; + } + } + + /** Get the identifier for a blank node + * + * Returns null if the resource is not a blank node. + * + * @return string The identifer for the bnode + */ + public function getBNodeId() + { + if (substr($this->uri, 0, 2) == '_:') { + return substr($this->uri, 2); + } else { + return null; + } + } + + /** Get a the prefix of the namespace that this resource is part of + * + * This method will return null the resource isn't part of any + * registered namespace. + * + * @return string The namespace prefix of the resource (e.g. foaf) + */ + public function prefix() + { + return RdfNamespace::prefixOfUri($this->uri); + } + + /** Get a shortened version of the resources URI. + * + * This method will return the full URI if the resource isn't part of any + * registered namespace. + * + * @return string The shortened URI of this resource (e.g. foaf:name) + */ + public function shorten() + { + return RdfNamespace::shorten($this->uri); + } + + /** Gets the local name of the URI of this resource + * + * The local name is defined as the part of the URI string + * after the last occurrence of the '#', ':' or '/' character. + * + * @return string The local name + */ + public function localName() + { + if (preg_match("|([^#:/]+)$|", $this->uri, $matches)) { + return $matches[1]; + } + } + + /** Parse the URI of the resource and return as a ParsedUri object + * + * @return ParsedUri + */ + public function parseUri() + { + return new ParsedUri($this->uri); + } + + /** Generates an HTML anchor tag, linking to this resource. + * + * If no text is given, then the URI also uses as the link text. + * + * @param string $text Text for the link. + * @param array $options Associative array of attributes for the anchor tag + * + * @throws \InvalidArgumentException + * @return string The HTML link string + */ + public function htmlLink($text = null, $options = array()) + { + $options = array_merge(array('href' => $this->uri), $options); + if ($text === null) { + $text = $this->uri; + } + + $html = " $value) { + if (!preg_match('/^[-\w]+$/', $key)) { + throw new \InvalidArgumentException( + "\$options should use valid attribute names as keys" + ); + } + + $html .= " ".htmlspecialchars($key)."=\"". + htmlspecialchars($value)."\""; + } + $html .= ">".htmlspecialchars($text).""; + + return $html; + } + + /** Returns the properties of the resource as an RDF/PHP associative array + * + * For example: + * array('type' => 'uri', 'value' => 'http://www.example.com/') + * + * @return array The properties of the resource + */ + public function toRdfPhp() + { + if ($this->isBNode()) { + return array('type' => 'bnode', 'value' => $this->uri); + } else { + return array('type' => 'uri', 'value' => $this->uri); + } + } + + /** Return pretty-print view of the resource + * + * @param string $format Either 'html' or 'text' + * @param string $color The colour of the text + * + * @return string + */ + public function dumpValue($format = 'html', $color = 'blue') + { + return Utils::dumpResourceValue($this, $format, $color); + } + + /** Magic method to return URI of resource when casted to string + * + * @return string The URI of the resource + */ + public function __toString() + { + return $this->uri; + } + + + + /** Throw can exception if the resource does not belong to a graph + * @ignore + */ + protected function checkHasGraph() + { + if (!$this->graph) { + throw new Exception( + 'EasyRdf\Resource is not part of a graph.' + ); + } + } + + /** Perform a load (download of remote URI) of the resource into the graph + * + * The document type is optional but should be specified if it + * can't be guessed or got from the HTTP headers. + * + * @param string $format Optional format of the data (eg. rdfxml) + * + * @return integer + */ + public function load($format = null) + { + $this->checkHasGraph(); + return $this->graph->load($this->uri, $format); + } + + /** Delete a property (or optionally just a specific value) + * + * @param string $property The name of the property (e.g. foaf:name) + * @param object $value The value to delete (null to delete all values) + * + * @return integer + */ + public function delete($property, $value = null) + { + $this->checkHasGraph(); + return $this->graph->delete($this->uri, $property, $value); + } + + /** Add values to for a property of the resource + * + * Example: + * $resource->add('prefix:property', 'value'); + * + * @param mixed $property The property name + * @param mixed $value The value for the property + * + * @return integer The number of values added (1 or 0) + */ + public function add($property, $value) + { + $this->checkHasGraph(); + return $this->graph->add($this->uri, $property, $value); + } + + /** Add a literal value as a property of the resource + * + * The value can either be a single value or an array of values. + * + * Example: + * $resource->add('dc:title', 'Title of Page'); + * + * @param mixed $property The property name + * @param mixed $values The value or values for the property + * @param string $lang The language of the literal + * + * @return integer The number of values added + */ + public function addLiteral($property, $values, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->addLiteral($this->uri, $property, $values, $lang); + } + + /** Add a resource as a property of the resource + * + * Example: + * $bob->add('foaf:knows', 'http://example.com/alice'); + * + * @param mixed $property The property name + * @param mixed $resource2 The resource to be the value of the property + * + * @return integer The number of values added (1 or 0) + */ + public function addResource($property, $resource2) + { + $this->checkHasGraph(); + return $this->graph->addResource($this->uri, $property, $resource2); + } + + /** Set value for a property + * + * The new value(s) will replace the existing values for the property. + * The name of the property should be a string. + * If you set a property to null or an empty array, then the property + * will be deleted. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value The value for the property. + * + * @return integer The number of values added (1 or 0) + */ + public function set($property, $value) + { + $this->checkHasGraph(); + return $this->graph->set($this->uri, $property, $value); + } + + /** Get a single value for a property + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * If $property is an array, then the first item in the array that matches + * a property that exists is returned. + * + * This method will return null if the property does not exist. + * + * @param string|array $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal or resource) + * @param string $lang The language to filter by (e.g. en) + * + * @return mixed A value associated with the property + */ + public function get($property, $type = null, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->get($this->uri, $property, $type, $lang); + } + + /** Get a single literal value for a property of the resource + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * This method will return null if there is not literal value for the + * property. + * + * @param string|array $property The name of the property (e.g. foaf:name) + * @param string $lang The language to filter by (e.g. en) + * + * @return Literal Literal value associated with the property + */ + public function getLiteral($property, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->get($this->uri, $property, 'literal', $lang); + } + + /** Get a single resource value for a property of the resource + * + * If multiple values are set for a property then the value returned + * may be arbitrary. + * + * This method will return null if there is not resource for the + * property. + * + * @param string|array $property The name of the property (e.g. foaf:name) + * + * @return self Resource associated with the property + */ + public function getResource($property) + { + $this->checkHasGraph(); + return $this->graph->get($this->uri, $property, 'resource'); + } + + /** Get all values for a property + * + * This method will return an empty array if the property does not exist. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal) + * @param string $lang The language to filter by (e.g. en) + * + * @return array An array of values associated with the property + */ + public function all($property, $type = null, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->all($this->uri, $property, $type, $lang); + } + + /** Get all literal values for a property of the resource + * + * This method will return an empty array if the resource does not + * has any literal values for that property. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param string $lang The language to filter by (e.g. en) + * + * @return array An array of values associated with the property + */ + public function allLiterals($property, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->all($this->uri, $property, 'literal', $lang); + } + + /** Get all resources for a property of the resource + * + * This method will return an empty array if the resource does not + * has any resources for that property. + * + * @param string $property The name of the property (e.g. foaf:name) + * + * @return array An array of values associated with the property + */ + public function allResources($property) + { + $this->checkHasGraph(); + return $this->graph->all($this->uri, $property, 'resource'); + } + + /** Count the number of values for a property of a resource + * + * This method will return 0 if the property does not exist. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param string $type The type of value to filter by (e.g. literal) + * @param string $lang The language to filter by (e.g. en) + * + * @return integer The number of values associated with the property + */ + public function countValues($property, $type = null, $lang = null) + { + $this->checkHasGraph(); + return $this->graph->countValues($this->uri, $property, $type, $lang); + } + + /** Concatenate all values for a property into a string. + * + * The default is to join the values together with a space character. + * This method will return an empty string if the property does not exist. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param string $glue The string to glue the values together with. + * @param string $lang The language to filter by (e.g. en) + * + * @return string Concatenation of all the values. + */ + public function join($property, $glue = ' ', $lang = null) + { + $this->checkHasGraph(); + return $this->graph->join($this->uri, $property, $glue, $lang); + } + + /** Get a list of the full URIs for the properties of this resource. + * + * This method will return an empty array if the resource has no properties. + * + * @return array Array of full URIs + */ + public function propertyUris() + { + $this->checkHasGraph(); + return $this->graph->propertyUris($this->uri); + } + + /** Get a list of all the shortened property names (qnames) for a resource. + * + * This method will return an empty array if the resource has no properties. + * + * @return array Array of shortened URIs + */ + public function properties() + { + $this->checkHasGraph(); + return $this->graph->properties($this->uri); + } + + /** Get a list of the full URIs for the properties that point to this resource. + * + * @return array Array of full property URIs + */ + public function reversePropertyUris() + { + $this->checkHasGraph(); + return $this->graph->reversePropertyUris($this->uri); + } + + /** Check to see if a property exists for this resource. + * + * This method will return true if the property exists. + * If the value parameter is given, then it will only return true + * if the value also exists for that property. + * + * @param string $property The name of the property (e.g. foaf:name) + * @param mixed $value An optional value of the property + * + * @return bool True if value the property exists. + */ + public function hasProperty($property, $value = null) + { + $this->checkHasGraph(); + return $this->graph->hasProperty($this->uri, $property, $value); + } + + /** Get a list of types for a resource. + * + * The types will each be a shortened URI as a string. + * This method will return an empty array if the resource has no types. + * + * @return array All types assocated with the resource (e.g. foaf:Person) + */ + public function types() + { + $this->checkHasGraph(); + return $this->graph->types($this->uri); + } + + /** Get a single type for a resource. + * + * The type will be a shortened URI as a string. + * If the resource has multiple types then the type returned + * may be arbitrary. + * This method will return null if the resource has no type. + * + * @return string A type assocated with the resource (e.g. foaf:Person) + */ + public function type() + { + $this->checkHasGraph(); + return $this->graph->type($this->uri); + } + + /** Get a single type for a resource, as a resource. + * + * The type will be returned as an EasyRdf\Resource. + * If the resource has multiple types then the type returned + * may be arbitrary. + * This method will return null if the resource has no type. + * + * @return Resource A type assocated with the resource. + */ + public function typeAsResource() + { + $this->checkHasGraph(); + return $this->graph->typeAsResource($this->uri); + } + + /** + * Get a list of types for a resource, as EasyRdf\Resource + * + * @return Resource[] + * @throws Exception + */ + public function typesAsResources() + { + $this->checkHasGraph(); + return $this->graph->typesAsResources($this->uri); + } + + /** Check if a resource is of the specified type + * + * @param string $type The type to check (e.g. foaf:Person) + * + * @return boolean True if resource is of specified type. + */ + public function isA($type) + { + $this->checkHasGraph(); + return $this->graph->isA($this->uri, $type); + } + + /** Add one or more rdf:type properties to the resource + * + * @param string $types One or more types to add (e.g. foaf:Person) + * + * @return integer The number of types added + */ + public function addType($types) + { + $this->checkHasGraph(); + return $this->graph->addType($this->uri, $types); + } + + /** Change the rdf:type property for the resource + * + * Note that the PHP class of the resource will not change. + * + * @param string $type The new type (e.g. foaf:Person) + * + * @return integer The number of types added + */ + public function setType($type) + { + $this->checkHasGraph(); + return $this->graph->setType($this->uri, $type); + } + + /** Get the primary topic of this resource. + * + * Returns null if no primary topic is available. + * + * @return Resource The primary topic of this resource. + */ + public function primaryTopic() + { + $this->checkHasGraph(); + return $this->graph->primaryTopic($this->uri); + } + + /** Get a human readable label for this resource + * + * This method will check a number of properties for the resource + * (in the order: skos:prefLabel, rdfs:label, foaf:name, dc:title) + * and return an approriate first that is available. If no label + * is available then it will return null. + * + * @param string|null $lang + * + * @return string A label for the resource. + */ + public function label($lang = null) + { + $this->checkHasGraph(); + return $this->graph->label($this->uri, $lang); + } + + /** Return a human readable view of the resource and its properties + * + * This method is intended to be a debugging aid and will + * print a resource and its properties. + * + * @param string $format Either 'html' or 'text' + * + * @return string + */ + public function dump($format = 'html') + { + $this->checkHasGraph(); + return $this->graph->dumpResource($this->uri, $format); + } + + /** Magic method to get a property of a resource + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * $value = $resource->title; + * + * @see EasyRdf\RdfNamespace::setDefault() + * + * @param string $name The name of the property + * + * @return string A single value for the named property + */ + public function __get($name) + { + return $this->graph->get($this->uri, $name); + } + + /** Magic method to set the value for a property of a resource + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * $resource->title = 'Title'; + * + * @see EasyRdf\RdfNamespace::setDefault() + * + * @param string $name The name of the property + * @param string $value The value for the property + * + * @return int + */ + public function __set($name, $value) + { + return $this->graph->set($this->uri, $name, $value); + } + + /** Magic method to check if a property exists + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * if (isset($resource->title)) { blah(); } + * + * @see EasyRdf\RdfNamespace::setDefault() + * + * @param string $name The name of the property + * + * @return bool + */ + public function __isset($name) + { + return $this->graph->hasProperty($this->uri, $name); + } + + /** Magic method to delete a property of the resource + * + * Note that only properties in the default namespace can be accessed in this way. + * + * Example: + * unset($resource->title); + * + * @see EasyRdf\RdfNamespace::setDefault() + * + * @param string $name The name of the property + * + * @return int + */ + public function __unset($name) + { + return $this->graph->delete($this->uri, $name); + } + + /** + * Whether a offset exists + * + * The return value will be casted to boolean if non-boolean was returned. + * + * Example: + * if(isset($resource['rdfs:label'])) { } + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset An offset to check for. + * + * @return boolean true on success or false on failure. + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + /** + * Offset to retrieve + * + * Example: + * $label = $resource['rdfs:label']; + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset The offset to retrieve. + * + * @return mixed Can return all value types. + */ + public function offsetGet($offset) + { + return $this->__get($offset); + } + + /** + * Offset to set + * + * Example: + * $resource['rdfs:label'] = 'label'; + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed The offset to assign the value to. + * @param mixed $value The value to set. + * + * @return void + */ + public function offsetSet($offset, $value) + { + $this->__set($offset, $value); + } + + /** + * Offset to unset + * + * Example: + * unset($resource['rdfs:label']); + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset The offset to unset. + * + * @return void + */ + public function offsetUnset($offset) + { + $this->__unset($offset); + } +} diff --git a/lib/Serialiser.php b/lib/Serialiser.php new file mode 100644 index 0000000..e51b19f --- /dev/null +++ b/lib/Serialiser.php @@ -0,0 +1,108 @@ +prefixes[$prefix] = true; + } + + /** + * Check and cleanup parameters passed to serialise() method + * @ignore + */ + protected function checkSerialiseParams(&$format) + { + if (is_null($format) or $format == '') { + throw new \InvalidArgumentException( + '$format cannot be null or empty' + ); + } elseif (is_object($format) and ($format instanceof Format)) { + $format = $format->getName(); + } elseif (!is_string($format)) { + throw new \InvalidArgumentException( + '$format should be a string or an EasyRdf\Format object' + ); + } + } + + /** + * Protected method to get the number of reverse properties for a resource + * If a resource only has a single property, the number of values for that + * property is returned instead. + * @ignore + */ + protected function reversePropertyCount($resource) + { + $properties = $resource->reversePropertyUris(); + $count = count($properties); + if ($count == 1) { + $property = $properties[0]; + return $resource->countValues("^<$property>"); + } else { + return $count; + } + } + + + /** + * Serialise an EasyRdf\Graph into desired format. + * + * @param Graph $graph An EasyRdf\Graph object. + * @param Format|string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + */ + abstract public function serialise(Graph $graph, $format, array $options = array()); +} diff --git a/lib/Serialiser/Arc.php b/lib/Serialiser/Arc.php new file mode 100644 index 0000000..32584ed --- /dev/null +++ b/lib/Serialiser/Arc.php @@ -0,0 +1,105 @@ + 'RDFXML', + 'turtle' => 'Turtle', + 'ntriples' => 'NTriples', + 'posh' => 'POSHRDF' + ); + + /** + * Constructor + */ + public function __construct() + { + if (!class_exists('ARC2')) { + throw new Exception('ARC2 dependency is not installed'); + } + } + + + /** + * Serialise an EasyRdf\Graph into RDF format of choice. + * + * @param Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + if (array_key_exists($format, self::$supportedTypes)) { + $className = self::$supportedTypes[$format]; + } else { + throw new Exception( + "EasyRdf\\Serialiser\\Arc does not support: {$format}" + ); + } + + /** @var \ARC2_RDFSerializer $serialiser */ + $serialiser = \ARC2::getSer($className); + if ($serialiser) { + return $serialiser->getSerializedIndex( + parent::serialise($graph, 'php') + ); + } else { + throw new Exception( + "ARC2 failed to get a $className serialiser." + ); + } + } +} + +Format::register('posh', 'poshRDF'); diff --git a/lib/Serialiser/GraphViz.php b/lib/Serialiser/GraphViz.php new file mode 100644 index 0000000..1206a29 --- /dev/null +++ b/lib/Serialiser/GraphViz.php @@ -0,0 +1,396 @@ + 'utf-8'); + + /** + * Set the path to the GraphViz 'dot' command + * + * Default is to search PATH for the command 'dot'. + * + * @param string $cmd The path to the 'dot' command. + * + * @return self + */ + public function setDotCommand($cmd) + { + $this->dotCommand = $cmd; + return $this; + } + + /** + * Get the path to the GraphViz 'dot' command + * + * The default value is simply 'dot' + * + * @return string The path to the 'dot' command. + */ + public function getDotCommand() + { + return $this->dotCommand; + } + + /** + * Turn on/off the option to display labels instead of URIs. + * + * When this option is turned on, then labels for resources will + * be displayed instead of the full URI of a resource. This makes + * it simpler to create friendly diagrams that non-technical people + * can understand. + * + * This option is turned off by default. + * + * @param bool $useLabels A boolean value to turn labels on and off + * + * @return GraphViz + */ + public function setUseLabels($useLabels) + { + $this->useLabels = $useLabels; + return $this; + } + + /** + * Get the state of the use labels option + * + * @return bool The current state of the use labels option + */ + public function getUseLabels() + { + return $this->useLabels; + } + + /** + * Turn on/off the option to only display nodes and edges with labels + * + * When this option is turned on, then only nodes (resources and literals) + * and edges (properties) will only be displayed if they have a label. You + * can use this option, to create concise, diagrams of your data, rather than + * the RDF. + * + * This option is turned off by default. + * + * @param bool $onlyLabelled A boolean value to enable/display only labelled items + * + * @return GraphViz + */ + public function setOnlyLabelled($onlyLabelled) + { + $this->onlyLabelled = $onlyLabelled; + return $this; + } + + /** + * Get the state of the only Only Labelled option + * + * @return bool The current state of the Only Labelled option + */ + public function getOnlyLabelled() + { + return $this->onlyLabelled; + } + + /** + * Set an attribute on the GraphViz graph + * + * Example: + * $serialiser->setAttribute('rotate', 90); + * + * See the GraphViz tool documentation for information about the + * available attributes. + * + * @param string $name The name of the attribute + * @param string $value The value for the attribute + * + * @return GraphViz + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + return $this; + } + + /** + * Get an attribute of the GraphViz graph + * + * @param string $name Attribute name + * + * @return string The value of the graph attribute + */ + public function getAttribute($name) + { + return $this->attributes[$name]; + } + + /** + * Convert an EasyRdf object into a GraphViz node identifier + * + * @ignore + */ + protected function nodeName($entity) + { + if ($entity instanceof Resource) { + if ($entity->isBNode()) { + return "B".$entity->getUri(); + } else { + return "R".$entity->getUri(); + } + } else { + return "L".$entity; + } + } + + /** + * Internal function to escape a string into DOT safe syntax + * + * @ignore + */ + protected function escape($input) + { + if (preg_match('/^([a-z_][a-z_0-9]*|-?(\.[0-9]+|[0-9]+(\.[0-9]*)?))$/i', $input)) { + return $input; + } else { + return '"'.str_replace( + array("\r\n", "\n", "\r", '"'), + array('\n', '\n', '\n', '\"'), + $input + ).'"'; + } + } + + /** + * Internal function to escape an associate array of attributes and + * turns it into a DOT notation string + * + * @ignore + */ + protected function escapeAttributes($array) + { + $items = array(); + foreach ($array as $k => $v) { + $items[] = $this->escape($k).'='.$this->escape($v); + } + return '['.implode(',', $items).']'; + } + + /** + * Internal function to create dot syntax line for either a node or an edge + * + * @ignore + */ + protected function serialiseRow($node1, $node2 = null, $attributes = array()) + { + $result = ' '.$this->escape($node1); + if ($node2) { + $result .= ' -> '.$this->escape($node2); + } + if (count($attributes)) { + $result .= ' '.$this->escapeAttributes($attributes); + } + return $result.";\n"; + } + + /** + * Internal function to serialise an EasyRdf\Graph into a DOT formatted string + * + * @ignore + */ + protected function serialiseDot(Graph $graph) + { + $result = "digraph {\n"; + + // Write the graph attributes + foreach ($this->attributes as $k => $v) { + $result .= ' '.$this->escape($k).'='.$this->escape($v).";\n"; + } + + // Go through each of the properties and write the edges + $nodes = array(); + $result .= "\n // Edges\n"; + foreach ($graph->resources() as $resource) { + $name1 = $this->nodeName($resource); + foreach ($resource->propertyUris() as $property) { + $label = null; + if ($this->useLabels) { + $label = $graph->resource($property)->label(); + } + if ($label === null) { + if ($this->onlyLabelled == true) { + continue; + } else { + $label = RdfNamespace::shorten($property); + } + } + foreach ($resource->all("<$property>") as $value) { + $name2 = $this->nodeName($value); + $nodes[$name1] = $resource; + $nodes[$name2] = $value; + $result .= $this->serialiseRow( + $name1, + $name2, + array('label' => $label) + ); + } + } + } + + ksort($nodes); + + $result .= "\n // Nodes\n"; + foreach ($nodes as $name => $node) { + $type = substr($name, 0, 1); + $label = ''; + if ($type == 'R') { + if ($this->useLabels) { + $label = $node->label(); + } + if (!$label) { + $label = $node->shorten(); + } + if (!$label) { + $label = $node->getURI(); + } + $result .= $this->serialiseRow( + $name, + null, + array( + 'URL' => $node->getURI(), + 'label' => $label, + 'shape' => 'ellipse', + 'color' => 'blue' + ) + ); + } elseif ($type == 'B') { + if ($this->useLabels) { + $label = $node->label(); + } + $result .= $this->serialiseRow( + $name, + null, + array( + 'label' => $label, + 'shape' => 'circle', + 'color' => 'green' + ) + ); + } else { + $result .= $this->serialiseRow( + $name, + null, + array( + 'label' => strval($node), + 'shape' => 'record', + ) + ); + } + } + + $result .= "}\n"; + + return $result; + } + + /** + * Internal function to render a graph into an image + * + * @ignore + */ + public function renderImage(Graph $graph, $format = 'png') + { + $dot = $this->serialiseDot($graph); + + return Utils::execCommandPipe( + $this->dotCommand, + array("-T$format"), + $dot + ); + } + + + /** + * Serialise an EasyRdf\Graph into a GraphViz dot document. + * + * Supported output format names: dot, gif, png, svg + * + * @param Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + switch ($format) { + case 'dot': + return $this->serialiseDot($graph); + case 'png': + case 'gif': + case 'svg': + return $this->renderImage($graph, $format); + default: + throw new Exception( + "EasyRdf\\Serialiser\\GraphViz does not support: {$format}" + ); + } + } +} diff --git a/lib/Serialiser/Json.php b/lib/Serialiser/Json.php new file mode 100644 index 0000000..91933bc --- /dev/null +++ b/lib/Serialiser/Json.php @@ -0,0 +1,75 @@ +toRdfPhp() as $resource => $properties) { + if (array_key_exists($resource, $nodes)) { + $node = $nodes[$resource]; + } else { + $node = $ld_graph->createNode($resource); + $nodes[$resource] = $node; + } + + foreach ($properties as $property => $values) { + foreach ($values as $value) { + if ($value['type'] == 'bnode' or $value['type'] == 'uri') { + if (array_key_exists($value['value'], $nodes)) { + $_value = $nodes[$value['value']]; + } else { + $_value = $ld_graph->createNode($value['value']); + $nodes[$value['value']] = $_value; + } + } elseif ($value['type'] == 'literal') { + if (isset($value['lang'])) { + $_value = new LD\LanguageTaggedString($value['value'], $value['lang']); + } elseif (isset($value['datatype'])) { + $_value = new LD\TypedValue($value['value'], $value['datatype']); + } else { + $_value = $value['value']; + } + } else { + throw new Exception( + "Unable to serialise object to JSON-LD: ".$value['type'] + ); + } + + if ($property == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { + $node->addType($_value); + } else { + $node->addPropertyValue($property, $_value); + } + } + } + } + + // OPTIONS + $use_native_types = !(isset($options['expand_native_types']) and $options['expand_native_types'] == true); + $should_compact = (isset($options['compact']) and $options['compact'] == true); + $should_frame = isset($options['frame']); + + // expanded form + $data = $ld_graph->toJsonLd($use_native_types); + + if ($should_frame) { + $data = LD\JsonLD::frame($data, $options['frame'], $options); + } + + if ($should_compact) { + // compact form + $compact_context = isset($options['context']) ? $options['context'] : null; + $compact_options = array( + 'useNativeTypes' => $use_native_types, + 'base' => $graph->getUri() + ); + + $data = LD\JsonLD::compact($data, $compact_context, $compact_options); + } + + return LD\JsonLD::toString($data); + } +} diff --git a/lib/Serialiser/Ntriples.php b/lib/Serialiser/Ntriples.php new file mode 100644 index 0000000..a69c81f --- /dev/null +++ b/lib/Serialiser/Ntriples.php @@ -0,0 +1,228 @@ +escChars[$c])) { + $this->escChars[$c] = $this->escapedChar($c); + } + + $result .= $this->escChars[$c]; + } + + return $result; + } + + /** + * @ignore + */ + protected function unicodeCharNo($cUtf) + { + $bl = strlen($cUtf); /* binary length */ + $r = 0; + switch ($bl) { + case 1: /* 0####### (0-127) */ + $r = ord($cUtf); + break; + case 2: /* 110##### 10###### = 192+x 128+x */ + $r = ((ord($cUtf[0]) - 192) * 64) + + (ord($cUtf[1]) - 128); + break; + case 3: /* 1110#### 10###### 10###### = 224+x 128+x 128+x */ + $r = ((ord($cUtf[0]) - 224) * 4096) + + ((ord($cUtf[1]) - 128) * 64) + + (ord($cUtf[2]) - 128); + break; + case 4: /* 1111#### 10###### 10###### 10###### = 240+x 128+x 128+x 128+x */ + $r = ((ord($cUtf[0]) - 240) * 262144) + + ((ord($cUtf[1]) - 128) * 4096) + + ((ord($cUtf[2]) - 128) * 64) + + (ord($cUtf[3]) - 128); + break; + } + return $r; + } + + /** + * @ignore + */ + protected function escapedChar($c) + { + $no = $this->unicodeCharNo($c); + + /* see http://www.w3.org/TR/rdf-testcases/#ntrip_strings */ + if ($no < 9) { + return "\\u" . sprintf('%04X', $no); /* #x0-#x8 (0-8) */ + } elseif ($no == 9) { + return '\t'; /* #x9 (9) */ + } elseif ($no == 10) { + return '\n'; /* #xA (10) */ + } elseif ($no < 13) { + return "\\u" . sprintf('%04X', $no); /* #xB-#xC (11-12) */ + } elseif ($no == 13) { + return '\r'; /* #xD (13) */ + } elseif ($no < 32) { + return "\\u" . sprintf('%04X', $no); /* #xE-#x1F (14-31) */ + } elseif ($no < 34) { + return $c; /* #x20-#x21 (32-33) */ + } elseif ($no == 34) { + return '\"'; /* #x22 (34) */ + } elseif ($no < 92) { + return $c; /* #x23-#x5B (35-91) */ + } elseif ($no == 92) { + return '\\\\'; // double backslash /* #x5C (92) */ + } elseif ($no < 127) { + return $c; /* #x5D-#x7E (93-126) */ + } elseif ($no < 65536) { + return "\\u" . sprintf('%04X', $no); /* #x7F-#xFFFF (128-65535) */ + } elseif ($no < 1114112) { + return "\\U" . sprintf('%08X', $no); /* #x10000-#x10FFFF (65536-1114111) */ + } else { + return ''; /* not defined => ignore */ + } + } + + /** + * @ignore + */ + protected function serialiseResource($res) + { + $escaped = $this->escapeString($res); + if (substr($res, 0, 2) == '_:') { + return $escaped; + } else { + return "<$escaped>"; + } + } + + /** + * Serialise an RDF value into N-Triples + * + * The value can either be an array in RDF/PHP form, or + * an EasyRdf\Literal or EasyRdf\Resource object. + * + * @param array|object $value An associative array or an object + * + * @throws Exception + * + * @return string The RDF value serialised to N-Triples + */ + public function serialiseValue($value) + { + if (is_object($value)) { + $value = $value->toRdfPhp(); + } + + if ($value['type'] == 'uri' or $value['type'] == 'bnode') { + return $this->serialiseResource($value['value']); + } elseif ($value['type'] == 'literal') { + $escaped = $this->escapeString($value['value']); + if (isset($value['lang'])) { + $lang = $this->escapeString($value['lang']); + return '"' . $escaped . '"' . '@' . $lang; + } elseif (isset($value['datatype'])) { + $datatype = $this->escapeString($value['datatype']); + return '"' . $escaped . '"' . "^^<$datatype>"; + } else { + return '"' . $escaped . '"'; + } + } else { + throw new Exception( + "Unable to serialise object of type '".$value['type']."' to ntriples: " + ); + } + } + + + /** + * Serialise an EasyRdf\Graph into N-Triples + * + * @param Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + if ($format == 'ntriples') { + $nt = ''; + foreach ($graph->toRdfPhp() as $resource => $properties) { + foreach ($properties as $property => $values) { + foreach ($values as $value) { + $nt .= $this->serialiseResource($resource)." "; + $nt .= "<" . $this->escapeString($property) . "> "; + $nt .= $this->serialiseValue($value)." .\n"; + } + } + } + return $nt; + } else { + throw new Exception( + __CLASS__." does not support: $format" + ); + } + } +} diff --git a/lib/Serialiser/Rapper.php b/lib/Serialiser/Rapper.php new file mode 100644 index 0000000..97412ba --- /dev/null +++ b/lib/Serialiser/Rapper.php @@ -0,0 +1,108 @@ +/dev/null", $output, $status); + if ($status != 0) { + throw new Exception( + "Failed to execute the command '$rapperCmd': " . join("\n", $output) + ); + } else { + $this->rapperCmd = $rapperCmd; + } + } + + + /** + * Serialise an EasyRdf\Graph to the RDF format of choice. + * + * @param \EasyRdf\Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + $ntriples = parent::serialise($graph, 'ntriples'); + + // Hack to produce more concise RDF/XML + if ($format == 'rdfxml') { + $format = 'rdfxml-abbrev'; + } + + return Utils::execCommandPipe( + $this->rapperCmd, + array( + '--quiet', + '--input', 'ntriples', + '--output', $format, + '-', 'unknown://' + ), + $ntriples + ); + } +} diff --git a/lib/Serialiser/RdfPhp.php b/lib/Serialiser/RdfPhp.php new file mode 100644 index 0000000..c4c786b --- /dev/null +++ b/lib/Serialiser/RdfPhp.php @@ -0,0 +1,77 @@ +toRdfPhp(); + } +} diff --git a/lib/Serialiser/RdfXml.php b/lib/Serialiser/RdfXml.php new file mode 100644 index 0000000..f3769c8 --- /dev/null +++ b/lib/Serialiser/RdfXml.php @@ -0,0 +1,256 @@ +propertyUris()); + $rpcount = $this->reversePropertyCount($obj); + $alreadyOutput = isset($this->outputtedResources[$obj->getUri()]); + + $tag = "{$indent}<{$property}"; + if ($obj->isBNode()) { + if ($alreadyOutput or $rpcount > 1 or $pcount == 0) { + $tag .= " rdf:nodeID=\"".htmlspecialchars($obj->getBNodeId()).'"'; + } + } else { + if ($alreadyOutput or $rpcount != 1 or $pcount == 0) { + $tag .= " rdf:resource=\"".htmlspecialchars($obj->getURI()).'"'; + } + } + + if ($alreadyOutput == false and $rpcount == 1 and $pcount > 0) { + $xml = $this->rdfxmlResource($obj, false, $depth+1); + if ($xml) { + return "$tag>$xml$indent\n\n"; + } else { + return ''; + } + } else { + return $tag."/>\n"; + } + } elseif (is_object($obj) and $obj instanceof Literal) { + $atrributes = ""; + $datatype = $obj->getDatatypeUri(); + if ($datatype) { + if ($datatype == self::RDF_XML_LITERAL) { + $atrributes .= " rdf:parseType=\"Literal\""; + $value = strval($obj); + } else { + $datatype = htmlspecialchars($datatype); + $atrributes .= " rdf:datatype=\"$datatype\""; + } + } elseif ($obj->getLang()) { + $atrributes .= ' xml:lang="'. + htmlspecialchars($obj->getLang()).'"'; + } + + // Escape the value + if (!isset($value)) { + $value = htmlspecialchars(strval($obj)); + } + + return "{$indent}<{$property}{$atrributes}>{$value}\n"; + } else { + throw new Exception( + "Unable to serialise object to xml: ".getType($obj) + ); + } + } + + /** + * Protected method to serialise a whole resource and its properties + * @ignore + */ + protected function rdfxmlResource($res, $showNodeId, $depth = 1) + { + // Keep track of the resources we have already serialised + if (isset($this->outputtedResources[$res->getUri()])) { + return ''; + } else { + $this->outputtedResources[$res->getUri()] = true; + } + + // If the resource has no properties - don't serialise it + $properties = $res->propertyUris(); + if (count($properties) == 0) { + return ''; + } + + $type = $res->type(); + if ($type) { + $this->addPrefix($type); + } else { + $type = 'rdf:Description'; + } + + $indent = str_repeat(' ', $depth); + $xml = "\n$indent<$type"; + if ($res->isBNode()) { + if ($showNodeId) { + $xml .= ' rdf:nodeID="'.htmlspecialchars($res->getBNodeId()).'"'; + } + } else { + $xml .= ' rdf:about="'.htmlspecialchars($res->getUri()).'"'; + } + $xml .= ">\n"; + + if ($res instanceof Container) { + foreach ($res as $item) { + $xml .= $this->rdfxmlObject('rdf:li', $item, $depth+1); + } + } else { + foreach ($properties as $property) { + $short = RdfNamespace::shorten($property, true); + if ($short) { + $this->addPrefix($short); + $objects = $res->all("<$property>"); + if ($short == 'rdf:type' && $type != 'rdf:Description') { + array_shift($objects); + } + foreach ($objects as $object) { + $xml .= $this->rdfxmlObject($short, $object, $depth+1); + } + } else { + throw new Exception( + "It is not possible to serialse the property ". + "'$property' to RDF/XML." + ); + } + } + } + $xml .= "$indent\n"; + + return $xml; + } + + + /** + * Method to serialise an EasyRdf\Graph to RDF/XML + * + * @param Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + if ($format != 'rdfxml') { + throw new Exception( + "EasyRdf\\Serialiser\\RdfXml does not support: {$format}" + ); + } + + // store of namespaces to be appended to the rdf:RDF tag + $this->prefixes = array('rdf' => true); + + // store of the resource URIs we have serialised + $this->outputtedResources = array(); + + $xml = ''; + + // Serialise URIs first + foreach ($graph->resources() as $resource) { + if (!$resource->isBnode()) { + $xml .= $this->rdfxmlResource($resource, true); + } + } + + // Serialise bnodes afterwards + foreach ($graph->resources() as $resource) { + if ($resource->isBnode()) { + $xml .= $this->rdfxmlResource($resource, true); + } + } + + // iterate through namepsaces array prefix and output a string. + $namespaceStr = ''; + foreach ($this->prefixes as $prefix => $count) { + $url = RdfNamespace::get($prefix); + + if (strlen($namespaceStr)) { + $namespaceStr .= "\n "; + } + + if (strlen($prefix) === 0) { + $namespaceStr .= ' xmlns="'.htmlspecialchars($url).'"'; + } else { + $namespaceStr .= ' xmlns:'.$prefix.'="'.htmlspecialchars($url).'"'; + } + } + + return "\n". + "\n" . $xml . "\n\n"; + } +} diff --git a/lib/Serialiser/Turtle.php b/lib/Serialiser/Turtle.php new file mode 100644 index 0000000..ba3ac1a --- /dev/null +++ b/lib/Serialiser/Turtle.php @@ -0,0 +1,389 @@ +', '\\>', $resourceIri); + return "<$escapedIri>"; + } + + /** + * Given a string, enclose in quotes and escape any quotes in the string. + * Strings containing tabs, linefeeds or carriage returns will be + * enclosed in three double quotes ("""). + * + * @param string $value + * + * @return string + */ + public static function quotedString($value) + { + if (preg_match('/[\t\n\r]/', $value)) { + $escaped = str_replace(array('\\', '"""'), array('\\\\', '\\"""'), $value); + + // Check if the last character is a trailing double quote, if so, escape it. + $pos = strrpos($escaped, '"'); + + if ($pos !== false && $pos + 1 == strlen($escaped)) { + $escaped = substr($escaped, 0, -1); + + $escaped .= '\"'; + } + + return '"""'.$escaped.'"""'; + } else { + $escaped = str_replace(array('\\', '"'), array('\\\\', '\\"'), $value); + return '"'.$escaped.'"'; + } + } + + /** + * Given a an EasyRdf\Resource or URI, convert it into a string, suitable to + * be written to a Turtle document. URIs will be shortened into CURIES + * where possible. + * + * @param Resource|string $resource The resource to convert to a Turtle string + * @param boolean $createNamespace If true, a new namespace may be created + * + * @return string + */ + public function serialiseResource($resource, $createNamespace = false) + { + if (is_object($resource)) { + if ($resource->isBNode()) { + return $resource->getUri(); + } + + $resource = $resource->getUri(); + } + + $short = RdfNamespace::shorten($resource, $createNamespace); + + if ($short) { + $this->addPrefix($short); + return $short; + } + + return self::escapeIri($resource); + } + + /** + * Given an EasyRdf\Literal object, convert it into a string, suitable to + * be written to a Turtle document. Supports multiline literals and literals with + * datatypes or languages. + * + * @param Literal $literal + * + * @return string + */ + public function serialiseLiteral($literal) + { + $value = strval($literal); + $quoted = self::quotedString($value); + + if ($datatype = $literal->getDatatypeUri()) { + if ($datatype == 'http://www.w3.org/2001/XMLSchema#integer') { + return sprintf('%d', $value); + } elseif ($datatype == 'http://www.w3.org/2001/XMLSchema#decimal') { + return sprintf('%s', $value); + } elseif ($datatype == 'http://www.w3.org/2001/XMLSchema#double') { + return sprintf('%e', $value); + } elseif ($datatype == 'http://www.w3.org/2001/XMLSchema#boolean') { + return sprintf('%s', $value); + } else { + $escaped = $this->serialiseResource($datatype, true); + return sprintf('%s^^%s', $quoted, $escaped); + } + } elseif ($lang = $literal->getLang()) { + return $quoted . '@' . $lang; + } else { + return $quoted; + } + } + + /** + * Convert an EasyRdf object into a string suitable to + * be written to a Turtle document. + * + * @param Resource|Literal $object + * + * @throws \InvalidArgumentException + * @return string + */ + public function serialiseObject($object) + { + if ($object instanceof Resource) { + return $this->serialiseResource($object); + } elseif ($object instanceof Literal) { + return $this->serialiseLiteral($object); + } else { + throw new \InvalidArgumentException( + "serialiseObject() requires \$object to be ". + 'of type EasyRdf\Resource or EasyRdf\Literal' + ); + } + } + + + /** + * Protected method to serialise a RDF collection + * @ignore + */ + protected function serialiseCollection($node, $indent) + { + $turtle = '('; + $count = 0; + while ($node) { + if ($id = $node->getBNodeId()) { + $this->outputtedBnodes[$id] = true; + } + + $value = $node->get('rdf:first'); + $node = $node->get('rdf:rest'); + if ($node and $node->hasProperty('rdf:first')) { + $count++; + } + + if ($value !== null) { + $serialised = $this->serialiseObject($value); + if ($count) { + $turtle .= "\n$indent $serialised"; + } else { + $turtle .= " ".$serialised; + } + } + } + if ($count) { + $turtle .= "\n$indent)"; + } else { + $turtle .= " )"; + } + return $turtle; + } + + /** + * Protected method to serialise the properties of a resource + * @ignore + */ + protected function serialiseProperties($res, $depth = 1) + { + $properties = $res->propertyUris(); + $indent = str_repeat(' ', ($depth*2)-1); + + $turtle = ''; + if (count($properties) > 1) { + $turtle .= "\n$indent"; + } + + $pCount = 0; + foreach ($properties as $property) { + if ($property === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') { + $pStr = 'a'; + } else { + $pStr = $this->serialiseResource($property, true); + } + + if ($pCount) { + $turtle .= " ;\n$indent"; + } + + $turtle .= ' ' . $pStr; + + $oCount = 0; + foreach ($res->all("<$property>") as $object) { + if ($oCount) { + $turtle .= ','; + } + + if ($object instanceof Collection) { + $turtle .= ' ' . $this->serialiseCollection($object, $indent); + } elseif ($object instanceof Resource and $object->isBNode()) { + $id = $object->getBNodeId(); + $rpcount = $this->reversePropertyCount($object); + if ($rpcount <= 1 and !isset($this->outputtedBnodes[$id])) { + // Nested unlabelled Blank Node + $this->outputtedBnodes[$id] = true; + $turtle .= ' ['; + $turtle .= $this->serialiseProperties($object, $depth+1); + $turtle .= ' ]'; + } else { + // Multiple properties pointing to this blank node + $turtle .= ' ' . $this->serialiseObject($object); + } + } else { + $turtle .= ' ' . $this->serialiseObject($object); + } + $oCount++; + } + $pCount++; + } + + if ($depth == 1) { + $turtle .= " ."; + if ($pCount > 1) { + $turtle .= "\n"; + } + } elseif ($pCount > 1) { + $turtle .= "\n" . str_repeat(' ', (($depth-1)*2)-1); + } + + return $turtle; + } + + /** + * @ignore + */ + protected function serialisePrefixes() + { + $turtle = ''; + foreach ($this->prefixes as $prefix => $count) { + $url = RdfNamespace::get($prefix); + $turtle .= "@prefix $prefix: <$url> .\n"; + } + return $turtle; + } + + /** + * @ignore + */ + protected function serialiseSubjects(Graph $graph, $filterType) + { + $turtle = ''; + foreach ($graph->resources() as $resource) { + /** @var $resource Resource */ + // If the resource has no properties - don't serialise it + $properties = $resource->propertyUris(); + if (count($properties) == 0) { + continue; + } + + // Is this node of the right type? + $thisType = $resource->isBNode() ? 'bnode' : 'uri'; + if ($thisType != $filterType) { + continue; + } + + if ($thisType == 'bnode') { + $id = $resource->getBNodeId(); + + if (isset($this->outputtedBnodes[$id])) { + // Already been serialised + continue; + } + + $this->outputtedBnodes[$id] = true; + $rpcount = $this->reversePropertyCount($resource); + + if ($rpcount == 0) { + $turtle .= '[]'; + } else { + $turtle .= $this->serialiseResource($resource); + } + } else { + $turtle .= $this->serialiseResource($resource); + } + + $turtle .= $this->serialiseProperties($resource); + $turtle .= "\n"; + } + return $turtle; + } + + + /** + * Serialise an EasyRdf\Graph to Turtle. + * + * @param Graph $graph An EasyRdf\Graph object. + * @param string $format The name of the format to convert to. + * @param array $options + * + * @return string The RDF in the new desired format. + * @throws Exception + */ + public function serialise(Graph $graph, $format, array $options = array()) + { + parent::checkSerialiseParams($format); + + if ($format != 'turtle' and $format != 'n3') { + throw new Exception( + "EasyRdf\\Serialiser\\Turtle does not support: {$format}" + ); + } + + $this->prefixes = array(); + $this->outputtedBnodes = array(); + + $turtle = ''; + $turtle .= $this->serialiseSubjects($graph, 'uri'); + $turtle .= $this->serialiseSubjects($graph, 'bnode'); + + if (count($this->prefixes)) { + return $this->serialisePrefixes() . "\n" . $turtle; + } else { + return $turtle; + } + } +} diff --git a/lib/Sparql/Client.php b/lib/Sparql/Client.php new file mode 100644 index 0000000..66ed473 --- /dev/null +++ b/lib/Sparql/Client.php @@ -0,0 +1,395 @@ +queryUri = $queryUri; + + if (strlen(parse_url($queryUri, PHP_URL_QUERY)) > 0) { + $this->queryUri_has_params = true; + } else { + $this->queryUri_has_params = false; + } + + if ($updateUri) { + $this->updateUri = $updateUri; + } else { + $this->updateUri = $queryUri; + } + } + + /** Get the URI of the SPARQL query endpoint + * + * @return string The query URI of the SPARQL endpoint + */ + public function getQueryUri() + { + return $this->queryUri; + } + + /** Get the URI of the SPARQL update endpoint + * + * @return string The query URI of the SPARQL endpoint + */ + public function getUpdateUri() + { + return $this->updateUri; + } + + /** + * @depredated + * @ignore + */ + public function getUri() + { + return $this->queryUri; + } + + /** Make a query to the SPARQL endpoint + * + * SELECT and ASK queries will return an object of type + * EasyRdf\Sparql\Result. + * + * CONSTRUCT and DESCRIBE queries will return an object + * of type EasyRdf\Graph. + * + * @param string $query The query string to be executed + * + * @return Result|\EasyRdf\Graph Result of the query. + */ + public function query($query) + { + return $this->request('query', $query); + } + + /** Count the number of triples in a SPARQL 1.1 endpoint + * + * Performs a SELECT query to estriblish the total number of triples. + * + * Counts total number of triples by default but a conditional triple pattern + * can be given to count of a subset of all triples. + * + * @param string $condition Triple-pattern condition for the count query + * + * @return integer The number of triples + */ + public function countTriples($condition = '?s ?p ?o') + { + // SELECT (COUNT(*) AS ?count) + // WHERE { + // {?s ?p ?o} + // UNION + // {GRAPH ?g {?s ?p ?o}} + // } + $result = $this->query('SELECT (COUNT(*) AS ?count) {'.$condition.'}'); + return $result[0]->count->getValue(); + } + + /** Get a list of named graphs from a SPARQL 1.1 endpoint + * + * Performs a SELECT query to get a list of the named graphs + * + * @param string $limit Optional limit to the number of results + * + * @return \EasyRdf\Resource[] array of objects for each named graph + */ + public function listNamedGraphs($limit = null) + { + $query = "SELECT DISTINCT ?g WHERE {GRAPH ?g {?s ?p ?o}}"; + if (!is_null($limit)) { + $query .= " LIMIT ".(int)$limit; + } + $result = $this->query($query); + + // Convert the result object into an array of resources + $graphs = array(); + foreach ($result as $row) { + array_push($graphs, $row->g); + } + return $graphs; + } + + /** Make an update request to the SPARQL endpoint + * + * Successful responses will return the HTTP response object + * + * Unsuccessful responses will throw an exception + * + * @param string $query The update query string to be executed + * + * @return \EasyRdf\Http\Response HTTP response + */ + public function update($query) + { + return $this->request('update', $query); + } + + public function insert($data, $graphUri = null) + { + #$this->updateData('INSET', + $query = 'INSERT DATA {'; + if ($graphUri) { + $query .= "GRAPH <$graphUri> {"; + } + $query .= $this->convertToTriples($data); + if ($graphUri) { + $query .= "}"; + } + $query .= '}'; + return $this->update($query); + } + + protected function updateData($operation, $data, $graphUri = null) + { + $query = "$operation DATA {"; + if ($graphUri) { + $query .= "GRAPH <$graphUri> {"; + } + $query .= $this->convertToTriples($data); + if ($graphUri) { + $query .= "}"; + } + $query .= '}'; + return $this->update($query); + } + + public function clear($graphUri, $silent = false) + { + $query = "CLEAR"; + if ($silent) { + $query .= " SILENT"; + } + if (preg_match('/^all|named|default$/i', $graphUri)) { + $query .= " $graphUri"; + } else { + $query .= " GRAPH <$graphUri>"; + } + return $this->update($query); + } + + /* + * Internal function to make an HTTP request to SPARQL endpoint + * + * @ignore + */ + protected function request($type, $query) + { + $processed_query = $this->preprocessQuery($query); + $response = $this->executeQuery($processed_query, $type); + + if (!$response->isSuccessful()) { + throw new Http\Exception("HTTP request for SPARQL query failed", 0, null, $response->getBody()); + } + + if ($response->getStatus() == 204) { + // No content + return $response; + } + + return $this->parseResponseToQuery($response); + } + + protected function convertToTriples($data) + { + if (is_string($data)) { + return $data; + } elseif (is_object($data) and $data instanceof Graph) { + # FIXME: insert Turtle when there is a way of seperateing out the prefixes + return $data->serialise('ntriples'); + } else { + throw new Exception( + "Don't know how to convert to triples for SPARQL query" + ); + } + } + + /** + * Adds missing prefix-definitions to the query + * + * Overriding classes may execute arbitrary query-alteration here + * + * @param string $query + * @return string + */ + protected function preprocessQuery($query) + { + // Check for undefined prefixes + $prefixes = ''; + foreach (RdfNamespace::namespaces() as $prefix => $uri) { + if (strpos($query, "{$prefix}:") !== false and + strpos($query, "PREFIX {$prefix}:") === false + ) { + $prefixes .= "PREFIX {$prefix}: <{$uri}>\n"; + } + } + + return $prefixes . $query; + } + + /** + * Build http-client object, execute request and return a response + * + * @param string $processed_query + * @param string $type Should be either "query" or "update" + * + * @return Http\Response|\Zend\Http\Response + * @throws Exception + */ + protected function executeQuery($processed_query, $type) + { + $client = Http::getDefaultHttpClient(); + $client->resetParameters(); + + // Tell the server which response formats we can parse + $sparql_results_types = array( + 'application/sparql-results+json' => 1.0, + 'application/sparql-results+xml' => 0.8 + ); + + if ($type == 'update') { + // accept anything, as "response body of a […] update request is implementation defined" + // @see http://www.w3.org/TR/sparql11-protocol/#update-success + $accept = Format::getHttpAcceptHeader($sparql_results_types); + $client->setHeaders('Accept', $accept); + + $client->setMethod('POST'); + $client->setUri($this->updateUri); + $client->setRawData($processed_query); + $client->setHeaders('Content-Type', 'application/sparql-update'); + } elseif ($type == 'query') { + $re = '(?:(?:\s*BASE\s*<.*?>\s*)|(?:\s*PREFIX\s+.+:\s*<.*?>\s*))*'. + '(CONSTRUCT|SELECT|ASK|DESCRIBE)[\W]'; + + $result = null; + $matched = mb_eregi($re, $processed_query, $result); + + if (false === $matched or count($result) !== 2) { + // non-standard query. is this something non-standard? + $query_verb = null; + } else { + $query_verb = strtoupper($result[1]); + } + + if ($query_verb === 'SELECT' or $query_verb === 'ASK') { + // only "results" + $accept = Format::formatAcceptHeader($sparql_results_types); + } elseif ($query_verb === 'CONSTRUCT' or $query_verb === 'DESCRIBE') { + // only "graph" + $accept = Format::getHttpAcceptHeader(); + } else { + // both + $accept = Format::getHttpAcceptHeader($sparql_results_types); + } + + $client->setHeaders('Accept', $accept); + + $encodedQuery = 'query=' . urlencode($processed_query); + + // Use GET if the query is less than 2kB + // 2046 = 2kB minus 1 for '?' and 1 for NULL-terminated string on server + if (strlen($encodedQuery) + strlen($this->queryUri) <= 2046) { + $delimiter = $this->queryUri_has_params ? '&' : '?'; + + $client->setMethod('GET'); + $client->setUri($this->queryUri . $delimiter . $encodedQuery); + } else { + // Fall back to POST instead (which is un-cacheable) + $client->setMethod('POST'); + $client->setUri($this->queryUri); + $client->setRawData($encodedQuery); + $client->setHeaders('Content-Type', 'application/x-www-form-urlencoded'); + } + } else { + throw new Exception('unexpected request-type: '.$type); + } + + return $client->request(); + } + + /** + * Parse HTTP-response object into a meaningful result-object. + * + * Can be overridden to do custom processing + * + * @param Http\Response|\Zend\Http\Response $response + * @return Graph|Result + */ + protected function parseResponseToQuery($response) + { + list($content_type,) = Utils::parseMimeType($response->getHeader('Content-Type')); + + if (strpos($content_type, 'application/sparql-results') === 0) { + $result = new Result($response->getBody(), $content_type); + return $result; + } else { + $result = new Graph($this->queryUri, $response->getBody(), $content_type); + return $result; + } + } +} diff --git a/lib/Sparql/Result.php b/lib/Sparql/Result.php new file mode 100644 index 0000000..91102e1 --- /dev/null +++ b/lib/Sparql/Result.php @@ -0,0 +1,390 @@ +parseXml($data); + } elseif ($mimeType == 'application/sparql-results+json') { + $this->parseJson($data); + } else { + throw new Exception( + "Unsupported SPARQL Query Results format: $mimeType" + ); + } + } + + /** Get the query result type (boolean/bindings) + * + * ASK queries return a result of type 'boolean'. + * SELECT query return a result of type 'bindings'. + * + * @return string The query result type. + */ + public function getType() + { + return $this->type; + } + + /** Return the boolean value of the query result + * + * If the query was of type boolean then this method will + * return either true or false. If the query was of some other + * type then this method will return null. + * + * @return boolean The result of the query. + */ + public function getBoolean() + { + return $this->boolean; + } + + /** Return true if the result of the query was true. + * + * @return boolean True if the query result was true. + */ + public function isTrue() + { + return $this->boolean == true; + } + + /** Return false if the result of the query was false. + * + * @return boolean True if the query result was false. + */ + public function isFalse() + { + return $this->boolean == false; + } + + /** Return the number of fields in a query result of type bindings. + * + * @return integer The number of fields. + */ + public function numFields() + { + return count($this->fields); + } + + /** Return the number of rows in a query result of type bindings. + * + * @return integer The number of rows. + */ + public function numRows() + { + return count($this); + } + + /** Get the field names in a query result of type bindings. + * + * @return array The names of the fields in the result. + */ + public function getFields() + { + return $this->fields; + } + + /** Return a human readable view of the query result. + * + * This method is intended to be a debugging aid and will + * return a pretty-print view of the query result. + * + * @param string $format Either 'text' or 'html' + * + * @throws Exception + * @return string + */ + public function dump($format = 'html') + { + if ($this->type == 'bindings') { + $result = ''; + if ($format == 'html') { + $result .= ""; + $result .= ""; + foreach ($this->fields as $field) { + $result .= ""; + } + $result .= ""; + foreach ($this as $row) { + $result .= ""; + foreach ($this->fields as $field) { + if (isset($row->$field)) { + $result .= ""; + } else { + $result .= ""; + } + } + $result .= ""; + } + $result .= "
". + "?$field
". + $row->$field->dumpValue($format)." 
"; + } else { + // First calculate the width of each comment + $colWidths = array(); + foreach ($this->fields as $field) { + $colWidths[$field] = strlen($field); + } + + $textData = array(); + foreach ($this as $row) { + $textRow = array(); + foreach ($row as $k => $v) { + $textRow[$k] = $v->dumpValue('text'); + $width = strlen($textRow[$k]); + if ($colWidths[$k] < $width) { + $colWidths[$k] = $width; + } + } + $textData[] = $textRow; + } + + // Create a horizontal rule + $hr = "+"; + foreach ($colWidths as $v) { + $hr .= "-".str_repeat('-', $v).'-+'; + } + + // Output the field names + $result .= "$hr\n|"; + foreach ($this->fields as $field) { + $result .= ' '.str_pad("?$field", $colWidths[$field]).' |'; + } + + // Output each of the rows + $result .= "\n$hr\n"; + foreach ($textData as $textRow) { + $result .= '|'; + foreach ($textRow as $k => $v) { + $result .= ' '.str_pad($v, $colWidths[$k]).' |'; + } + $result .= "\n"; + } + $result .= "$hr\n"; + } + return $result; + } elseif ($this->type == 'boolean') { + $str = ($this->boolean ? 'true' : 'false'); + if ($format == 'html') { + return "

Result: $str

"; + } else { + return "Result: $str"; + } + } else { + throw new Exception( + "Failed to dump SPARQL Query Results format, unknown type: ". $this->type + ); + } + } + + /** Create a new EasyRdf\Resource or EasyRdf\Literal depending + * on the type of data passed in. + * + * @ignore + */ + protected function newTerm($data) + { + switch ($data['type']) { + case 'bnode': + return new Resource('_:'.$data['value']); + case 'uri': + return new Resource($data['value']); + case 'literal': + case 'typed-literal': + return Literal::create($data); + default: + throw new Exception( + "Failed to parse SPARQL Query Results format, unknown term type: ". + $data['type'] + ); + } + } + + /** Parse a SPARQL result in the XML format into the object. + * + * @ignore + */ + protected function parseXml($data) + { + $doc = new \DOMDocument(); + $doc->loadXML($data); + + # Check for valid root node. + if ($doc->hasChildNodes() == false or + $doc->childNodes->length != 1 or + $doc->firstChild->nodeName != 'sparql' or + $doc->firstChild->namespaceURI != self::SPARQL_XML_RESULTS_NS) { + throw new Exception( + "Incorrect root node in SPARQL XML Query Results format" + ); + } + + # Is it the result of an ASK query? + $boolean = $doc->getElementsByTagName('boolean'); + if ($boolean->length) { + $this->type = 'boolean'; + $value = $boolean->item(0)->nodeValue; + $this->boolean = $value == 'true' ? true : false; + return; + } + + # Get a list of variables from the header + $head = $doc->getElementsByTagName('head'); + if ($head->length) { + $variables = $head->item(0)->getElementsByTagName('variable'); + foreach ($variables as $variable) { + $this->fields[] = $variable->getAttribute('name'); + } + } + + # Is it the result of a SELECT query? + $resultstag = $doc->getElementsByTagName('results'); + if ($resultstag->length) { + $this->type = 'bindings'; + $results = $resultstag->item(0)->getElementsByTagName('result'); + foreach ($results as $result) { + $bindings = $result->getElementsByTagName('binding'); + $t = new \stdClass(); + foreach ($bindings as $binding) { + $key = $binding->getAttribute('name'); + foreach ($binding->childNodes as $node) { + if ($node->nodeType != XML_ELEMENT_NODE) { + continue; + } + $t->$key = $this->newTerm( + array( + 'type' => $node->nodeName, + 'value' => $node->nodeValue, + 'lang' => $node->getAttribute('xml:lang'), + 'datatype' => $node->getAttribute('datatype') + ) + ); + break; + } + } + $this[] = $t; + } + return; + } + + throw new Exception( + "Failed to parse SPARQL XML Query Results format" + ); + } + + /** Parse a SPARQL result in the JSON format into the object. + * + * @ignore + */ + protected function parseJson($data) + { + // Decode JSON to an array + $data = json_decode($data, true); + + if (isset($data['boolean'])) { + $this->type = 'boolean'; + $this->boolean = $data['boolean']; + } elseif (isset($data['results'])) { + $this->type = 'bindings'; + if (isset($data['head']['vars'])) { + $this->fields = $data['head']['vars']; + } + + foreach ($data['results']['bindings'] as $row) { + $t = new \stdClass(); + foreach ($row as $key => $value) { + $t->$key = $this->newTerm($value); + } + $this[] = $t; + } + } else { + throw new Exception( + "Failed to parse SPARQL JSON Query Results format" + ); + } + } + + /** Magic method to return value of the result to string + * + * If this is a boolean result then it will return 'true' or 'false'. + * If it is a bindings type, then it will dump as a text based table. + * + * @return string A string representation of the result. + */ + public function __toString() + { + if ($this->type == 'boolean') { + return $this->boolean ? 'true' : 'false'; + } else { + return $this->dump('text'); + } + } +} diff --git a/lib/TypeMapper.php b/lib/TypeMapper.php new file mode 100644 index 0000000..43a8fa4 --- /dev/null +++ b/lib/TypeMapper.php @@ -0,0 +1,175 @@ +$short"; + } else { + return "$escaped"; + } + } else { + if ($short) { + return $short; + } else { + return $resource; + } + } + } + + /** Return pretty-print view of a literal + * + * This method is mainly intended for internal use and is used by + * EasyRdf\Graph and EasyRdf\Sparql\Result to format a literal + * for display. + * + * @param mixed $literal An EasyRdf\Literal object or an associative array + * @param string $format Either 'html' or 'text' + * @param string $color The colour of the text + * + * @throws \InvalidArgumentException + * @return string + */ + public static function dumpLiteralValue($literal, $format = 'html', $color = 'black') + { + if (!preg_match('/^#?[-\w]+$/', $color)) { + throw new \InvalidArgumentException( + "\$color must be a legal color code or name" + ); + } + + if (is_object($literal)) { + $literal = $literal->toRdfPhp(); + } elseif (!is_array($literal)) { + $literal = array('value' => $literal); + } + + $text = '"'.$literal['value'].'"'; + if (isset($literal['lang'])) { + $text .= '@' . $literal['lang']; + } + if (isset($literal['datatype'])) { + $short = RdfNamespace::shorten($literal['datatype']); + if ($short) { + $text .= "^^$short"; + } else { + $text .= "^^<".$literal['datatype'].">"; + } + } + + if ($format == 'html') { + return "". + htmlentities($text, ENT_COMPAT, "UTF-8"). + ""; + } else { + return $text; + } + } + + /** Clean up and split a mime-type up into its parts + * + * @param string $mimeType A MIME Type, optionally with parameters + * + * @return array $type, $parameters + */ + public static function parseMimeType($mimeType) + { + $parts = explode(';', strtolower($mimeType)); + $type = trim(array_shift($parts)); + $params = array(); + foreach ($parts as $part) { + if (preg_match('/^\s*(\w+)\s*=\s*(.+?)\s*$/', $part, $matches)) { + $params[$matches[1]] = $matches[2]; + } + } + return array($type, $params); + } + + /** Execute a command as a pipe + * + * The proc_open() function is used to open a pipe to a + * a command line process, writing $input to STDIN, returning STDOUT + * and throwing an exception if anything is written to STDERR or the + * process returns non-zero. + * + * @param string $command The command to execute + * @param array $args Optional list of arguments to pass to the command + * @param string $input Optional buffer to send to the command + * @param string $dir Path to directory to run command in (defaults to /tmp) + * + * @throws Exception + * @return string The result of the command, printed to STDOUT + */ + public static function execCommandPipe($command, $args = null, $input = null, $dir = null) + { + $descriptorspec = array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w') + ); + + // Use the system tmp directory by default + if (!$dir) { + $dir = sys_get_temp_dir(); + } + + if (is_array($args)) { + $fullCommand = implode( + ' ', + array_map('escapeshellcmd', array_merge(array($command), $args)) + ); + } else { + $fullCommand = escapeshellcmd($command); + if ($args) { + $fullCommand .= ' '.escapeshellcmd($args); + } + } + + $process = proc_open($fullCommand, $descriptorspec, $pipes, $dir); + if (is_resource($process)) { + // $pipes now looks like this: + // 0 => writeable handle connected to child stdin + // 1 => readable handle connected to child stdout + // 2 => readable handle connected to child stderr + + if ($input) { + fwrite($pipes[0], $input); + } + fclose($pipes[0]); + + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $error = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + // It is important that you close any pipes before calling + // proc_close in order to avoid a deadlock + $returnValue = proc_close($process); + if ($returnValue) { + throw new Exception( + "Error while executing command $command: ".$error + ); + } + } else { + throw new Exception( + "Failed to execute command $command" + ); + } + + return $output; + } +} diff --git a/scripts/copyright_updater.php b/scripts/copyright_updater.php new file mode 100644 index 0000000..16b68c6 --- /dev/null +++ b/scripts/copyright_updater.php @@ -0,0 +1,64 @@ + Date: Wed, 10 Feb 2021 16:03:28 +0100 Subject: Import php-easyrdf_1.0.0-2.debian.tar.xz [dgit import tarball php-easyrdf 1.0.0-2 php-easyrdf_1.0.0-2.debian.tar.xz] --- changelog | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ clean | 1 + control | 48 +++++++++++++++++++++++++++++++ copyright | 67 ++++++++++++++++++++++++++++++++++++++++++++ docs | 1 + gbp.conf | 5 ++++ install | 1 + rules | 11 ++++++++ source/format | 1 + tests/control | 14 +++++++++ tests/data/foaf.rdf | 42 +++++++++++++++++++++++++++ tests/rapper.php | 14 +++++++++ tests/rapper.sh | 20 +++++++++++++ tests/test.php | 17 +++++++++++ tests/test.sh | 17 +++++++++++ upstream/metadata | 5 ++++ watch | 2 ++ 17 files changed, 347 insertions(+) create mode 100644 changelog create mode 100644 clean create mode 100644 control create mode 100644 copyright create mode 100644 docs create mode 100644 gbp.conf create mode 100644 install create mode 100755 rules create mode 100644 source/format create mode 100644 tests/control create mode 100644 tests/data/foaf.rdf create mode 100644 tests/rapper.php create mode 100644 tests/rapper.sh create mode 100644 tests/test.php create mode 100644 tests/test.sh create mode 100644 upstream/metadata create mode 100644 watch diff --git a/changelog b/changelog new file mode 100644 index 0000000..1c8e431 --- /dev/null +++ b/changelog @@ -0,0 +1,81 @@ +php-easyrdf (1.0.0-2) unstable; urgency=medium + + * Change gbp to use debian/bullseye for the maintenance branch + * Test server output before using it during smoke testing + + -- Marco Villegas Wed, 10 Feb 2021 10:03:28 -0500 + +php-easyrdf (1.0.0-1) unstable; urgency=medium + + * New upstream release 1.0.0. + Lots of modernization, highligths from release notes are support for newer + php versions, use of PSR-4 autoloader. Plese see CHANGELOG.md for more + information. + Special thanks to Elías Alejandro Año Mendoza for unblocking me by + pointing to the right next step. + * Start autoload generation via phpab. + * Change install path now that namespaces are in place. + * Update smoke tests to follow upstream and new autoload method. + Mark them with autopkgtest superficial restriction. + * Copyright update. + * Add readme as documentation + * Setup gbp to sign tags created by import-orig or import-ref + * Remove php-mbstring from dependencies, it is autoadded. + * Use Tests instead of Test-command for package tests. + + -- Marco Villegas Tue, 19 Jan 2021 09:47:36 -0500 + +php-easyrdf (0.9.1-5) unstable; urgency=medium + + * Change upstream locations, now the project is not under the njh user, but + instead it is under the github organization easyrdf. + * Use secure URI in Homepage field. + * Change slightly uscan watch expression to avoid the problem with + 1.0.0-rc.1 recognized as higher than 1.0.0. + * Set upstream metadata fields: Repository, Repository-Browse. + * Set Rules-Requires-Root: no. + * Run smoke test on build, changing the test script a bit to accommodate for + the case. + * Stop depending on network access at smoke tests. + + -- Marco Villegas Fri, 28 Aug 2020 18:07:59 -0500 + +php-easyrdf (0.9.1-4) unstable; urgency=medium + + * debian/control: + - Add raptor2-utils as suggested, hinting one of the features in the + library. + - Add graphviz as suggested, hinting a way to process files produced via + rapper serialiser. + - Add a smoke test using rapper to output a dot file, and later use + graphviz to convert it into an image. + - Remove pkg-php-tools build dependency version constraint, not needed. + - Bump debhelper version to newest. + - Remove composer from build dependencies. + Closes: #934913. + + -- Marco Villegas Wed, 26 Aug 2020 10:59:54 -0500 + +php-easyrdf (0.9.1-3) unstable; urgency=medium + + * debian/upstream/metadata: + - Started upstream metadata file. + + -- Marco Villegas Mon, 12 Aug 2019 05:07:06 -0300 + +php-easyrdf (0.9.1-2) unstable; urgency=medium + + * debian/control: + - Depend on php-xml, since it is used internally by the library. + * debian/tests/control: + - Depend on php-cli, which is used for the execution. + + -- Marco Villegas Tue, 06 Aug 2019 21:11:22 -0300 + +php-easyrdf (0.9.1-1) unstable; urgency=medium + + * Initial release. + Add the first RDF consumer and producer php library in debian. + Closes: #932244. + + -- Marco Villegas Sat, 20 Jul 2019 11:53:08 -0300 diff --git a/clean b/clean new file mode 100644 index 0000000..6d47d7c --- /dev/null +++ b/clean @@ -0,0 +1 @@ +lib/autoload.php diff --git a/control b/control new file mode 100644 index 0000000..8a9c288 --- /dev/null +++ b/control @@ -0,0 +1,48 @@ +Source: php-easyrdf +Priority: optional +Maintainer: Debian PHP PEAR Maintainers +Uploaders: + Marco Villegas , +Build-Depends: + curl, + debhelper-compat (= 13), + phpab, + pkg-php-tools, +Standards-Version: 4.5.0 +Section: php +Homepage: https://www.easyrdf.org/ +Vcs-Browser: https://salsa.debian.org/php-team/pear/php-easyrdf +Vcs-Git: https://salsa.debian.org/php-team/pear/php-easyrdf.git +Rules-Requires-Root: no + +Package: php-easyrdf +Architecture: all +Depends: + php-xml, + ${misc:Depends}, + ${phpcomposer:Debian-require}, +Suggests: + ${phpcomposer:Debian-suggest}, + graphviz, + raptor2-utils, +Provides: + ${phpcomposer:Debian-provide}, +Description: PHP library to consume and produce RDF + EasyRdf is a PHP library to consume and produce RDF. + Resource Description Framework, RDF, is an official W3C Recommendation + for Semantic Web data models. + . + After parsing EasyRdf builds up a graph of PHP objects that can then be + walked around to get the data to be placed on the page. + Dump methods are available to inspect what data is available during + development. + . + Data is typically loaded into a EasyRdf_Graph object from source RDF + documents, loaded from the web via HTTP. + The EasyRdf_GraphStore class can load and save data on a SPARQL 1.1 + Graph Store. + . + SPARQL queries can be made over HTTP to a Triplestore using the + EasyRdf_Sparql_Client class. + SELECT and ASK queries will return an EasyRdf_Sparql_Result object and + CONSTRUCT and DESCRIBE queries will return an EasyRdf_Graph object. diff --git a/copyright b/copyright new file mode 100644 index 0000000..47c6691 --- /dev/null +++ b/copyright @@ -0,0 +1,67 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: EasyRdf +Upstream-Contact: Nicholas Humfrey +Source: https://github.com/njh/easyrdf + +Files: * +Copyright: + 1997-2013 Aduna + 2004-2010 Benjamin Nowack + 2005-2009 Zend Technologies USA Inc. + 2009-2020 Nicholas J Humfrey + 2014 Alexey Zakhlestin + 2014 Markus Lanthaler +License: BSD-3-Clause~Nicholas + +Files: debian/* +Copyright: + 2019 Marco Villegas +License-Grant: + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. +License: GPL-3+ + +License: BSD-3-Clause~Nicholas + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The name of the author 'Nicholas J Humfrey" may be used to endorse + or promote products derived from this software without specific prior + written permission. + . + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: GPL-3+ + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + . + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + Public License for more details. + . + You should have received a copy of the GNU General Public License along + with this program. If not, see . + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + `/usr/share/common-licenses/GPL-3'. diff --git a/docs b/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/docs @@ -0,0 +1 @@ +README.md diff --git a/gbp.conf b/gbp.conf new file mode 100644 index 0000000..48d3504 --- /dev/null +++ b/gbp.conf @@ -0,0 +1,5 @@ +[DEFAULT] +pristine-tar = True +pristine-tar-commit = True +sign-tags=True +debian-branch = debian/bullseye diff --git a/install b/install new file mode 100644 index 0000000..71c4804 --- /dev/null +++ b/install @@ -0,0 +1 @@ +lib/* usr/share/php/EasyRdf diff --git a/rules b/rules new file mode 100755 index 0000000..b158cdd --- /dev/null +++ b/rules @@ -0,0 +1,11 @@ +#!/usr/bin/make -f +%: + dh $@ --with phpcomposer + +execute_after_dh_auto_build: + phpab --output lib/autoload.php \ + lib + +override_dh_auto_test: + dh_auto_test -- --with phpcomposer + sh debian/tests/test.sh -l diff --git a/source/format b/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/tests/control b/tests/control new file mode 100644 index 0000000..358fac3 --- /dev/null +++ b/tests/control @@ -0,0 +1,14 @@ +Tests: test.sh +Depends: @, + curl, + php-cli, +Restrictions: allow-stderr, superficial + +Tests: rapper.sh +Depends: @, + curl, + file, + graphviz, + php-cli, + raptor2-utils, +Restrictions: allow-stderr, superficial diff --git a/tests/data/foaf.rdf b/tests/data/foaf.rdf new file mode 100644 index 0000000..8d3e311 --- /dev/null +++ b/tests/data/foaf.rdf @@ -0,0 +1,42 @@ + + + + + Jane Fiction's FOAF File + + + + + + Jane Fiction + Dr + Jane + Fiction + Female + + + jjff + + + Debian + Debian + + + + + Linux + Linux + + + + + + Homepage of Jane Fiction + + + diff --git a/tests/rapper.php b/tests/rapper.php new file mode 100644 index 0000000..7939398 --- /dev/null +++ b/tests/rapper.php @@ -0,0 +1,14 @@ +load(); + +$data = $foaf->serialise('dot'); +if (!is_scalar($data)) { + exit(1); +} +echo $data, PHP_EOL; diff --git a/tests/rapper.sh b/tests/rapper.sh new file mode 100644 index 0000000..b86157b --- /dev/null +++ b/tests/rapper.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -e + +# Start a web server to serve the file. +php --server localhost:10101 --docroot debian/tests/data & +SERVER_PID=$! + +# A graphviz source file can be created. +php debian/tests/rapper.php > sample.dot + +kill -9 $SERVER_PID + +# A valid dot can be used by graphviz. +dot -Tpng sample.dot > sample.png +MIME_TYPE=`file --mime-type --brief sample.png` +if [ "$MIME_TYPE" != 'image/png' ] +then + exit 1; +fi diff --git a/tests/test.php b/tests/test.php new file mode 100644 index 0000000..59d956b --- /dev/null +++ b/tests/test.php @@ -0,0 +1,17 @@ +load(); +$author = $foaf->primaryTopic(); +echo 'Main author name is: ', $author->get('foaf:name'), PHP_EOL; diff --git a/tests/test.sh b/tests/test.sh new file mode 100644 index 0000000..d9b8b39 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +# Start a web server to serve the file. +php --server localhost:10101 --docroot debian/tests/data & +SERVER_PID=$! +set +e +curl --fail --silent --output /dev/null http://localhost:10101/foaf.rdf +if [ $? -ne 0 ] +then + # Likely 22, the server may not be ready, give it a bit of time. + sleep 2 +fi +set -e +php debian/tests/test.php $@ +kill -9 $SERVER_PID diff --git a/upstream/metadata b/upstream/metadata new file mode 100644 index 0000000..539585a --- /dev/null +++ b/upstream/metadata @@ -0,0 +1,5 @@ +Bug-Database: https://github.com/easyrdf/easyrdf/issues +Bug-Submit: https://github.com/easyrdf/easyrdf/issues/new +Contact: https://github.com/easyrdf/easyrdf/issues/new +Repository: https://github.com/easyrdf/easyrdf.git +Repository-Browse: https://github.com/easyrdf/easyrdf diff --git a/watch b/watch new file mode 100644 index 0000000..f9c9755 --- /dev/null +++ b/watch @@ -0,0 +1,2 @@ +version=4 +opts="uversionmangle=s/-?([^\d.]+)/~$1/;tr/A-Z/a-z/" https://github.com/easyrdf/easyrdf/tags .*/archive/([\d\S]+).tar.gz -- cgit v1.2.3