diff options
-rw-r--r-- | .travis.yml | 22 | ||||
-rw-r--r-- | CHANGELOG.md | 393 | ||||
-rw-r--r-- | CODE_OF_CONDUCT.md | 130 | ||||
-rw-r--r-- | DEVELOPER.md | 31 | ||||
-rw-r--r-- | LICENSE.md | 420 | ||||
-rw-r--r-- | README.md | 126 | ||||
-rw-r--r-- | composer.json | 49 | ||||
-rw-r--r-- | debian/changelog (renamed from changelog) | 0 | ||||
-rw-r--r-- | debian/clean (renamed from clean) | 0 | ||||
-rw-r--r-- | debian/control (renamed from control) | 0 | ||||
-rw-r--r-- | debian/copyright (renamed from copyright) | 0 | ||||
-rw-r--r-- | debian/docs (renamed from docs) | 0 | ||||
-rw-r--r-- | debian/gbp.conf (renamed from gbp.conf) | 0 | ||||
-rw-r--r-- | debian/install (renamed from install) | 0 | ||||
-rwxr-xr-x | debian/rules (renamed from rules) | 0 | ||||
-rw-r--r-- | debian/source/format (renamed from source/format) | 0 | ||||
-rw-r--r-- | debian/tests/control (renamed from tests/control) | 0 | ||||
-rw-r--r-- | debian/tests/data/foaf.rdf (renamed from tests/data/foaf.rdf) | 0 | ||||
-rw-r--r-- | debian/tests/rapper.php (renamed from tests/rapper.php) | 0 | ||||
-rw-r--r-- | debian/tests/rapper.sh (renamed from tests/rapper.sh) | 0 | ||||
-rw-r--r-- | debian/tests/test.php (renamed from tests/test.php) | 0 | ||||
-rw-r--r-- | debian/tests/test.sh (renamed from tests/test.sh) | 0 | ||||
-rw-r--r-- | debian/upstream/metadata (renamed from upstream/metadata) | 0 | ||||
-rw-r--r-- | debian/watch (renamed from watch) | 0 | ||||
-rw-r--r-- | doap.php | 44 | ||||
-rw-r--r-- | lib/Collection.php | 338 | ||||
-rw-r--r-- | lib/Container.php | 234 | ||||
-rw-r--r-- | lib/Exception.php | 51 | ||||
-rw-r--r-- | lib/Format.php | 735 | ||||
-rw-r--r-- | lib/Graph.php | 1753 | ||||
-rw-r--r-- | lib/GraphStore.php | 312 | ||||
-rw-r--r-- | lib/Http.php | 85 | ||||
-rw-r--r-- | lib/Http/Client.php | 575 | ||||
-rw-r--r-- | lib/Http/Exception.php | 18 | ||||
-rw-r--r-- | lib/Http/Response.php | 431 | ||||
-rw-r--r-- | lib/Isomorphic.php | 437 | ||||
-rw-r--r-- | lib/Literal.php | 342 | ||||
-rw-r--r-- | lib/Literal/Boolean.php | 94 | ||||
-rw-r--r-- | lib/Literal/Date.php | 140 | ||||
-rw-r--r-- | lib/Literal/DateTime.php | 119 | ||||
-rw-r--r-- | lib/Literal/Decimal.php | 127 | ||||
-rw-r--r-- | lib/Literal/HTML.php | 72 | ||||
-rw-r--r-- | lib/Literal/HexBinary.php | 93 | ||||
-rw-r--r-- | lib/Literal/Integer.php | 69 | ||||
-rw-r--r-- | lib/Literal/XML.php | 72 | ||||
-rw-r--r-- | lib/ParsedUri.php | 340 | ||||
-rw-r--r-- | lib/Parser.php | 154 | ||||
-rw-r--r-- | lib/Parser/Arc.php | 99 | ||||
-rw-r--r-- | lib/Parser/Exception.php | 77 | ||||
-rw-r--r-- | lib/Parser/Json.php | 158 | ||||
-rw-r--r-- | lib/Parser/JsonLd.php | 127 | ||||
-rw-r--r-- | lib/Parser/Ntriples.php | 218 | ||||
-rw-r--r-- | lib/Parser/Rapper.php | 107 | ||||
-rw-r--r-- | lib/Parser/RdfPhp.php | 134 | ||||
-rw-r--r-- | lib/Parser/RdfXml.php | 815 | ||||
-rw-r--r-- | lib/Parser/Rdfa.php | 728 | ||||
-rw-r--r-- | lib/Parser/Turtle.php | 1362 | ||||
-rw-r--r-- | lib/RdfNamespace.php | 443 | ||||
-rw-r--r-- | lib/Resource.php | 828 | ||||
-rw-r--r-- | lib/Serialiser.php | 108 | ||||
-rw-r--r-- | lib/Serialiser/Arc.php | 105 | ||||
-rw-r--r-- | lib/Serialiser/GraphViz.php | 396 | ||||
-rw-r--r-- | lib/Serialiser/Json.php | 75 | ||||
-rw-r--r-- | lib/Serialiser/JsonLd.php | 148 | ||||
-rw-r--r-- | lib/Serialiser/Ntriples.php | 228 | ||||
-rw-r--r-- | lib/Serialiser/Rapper.php | 108 | ||||
-rw-r--r-- | lib/Serialiser/RdfPhp.php | 77 | ||||
-rw-r--r-- | lib/Serialiser/RdfXml.php | 256 | ||||
-rw-r--r-- | lib/Serialiser/Turtle.php | 389 | ||||
-rw-r--r-- | lib/Sparql/Client.php | 395 | ||||
-rw-r--r-- | lib/Sparql/Result.php | 390 | ||||
-rw-r--r-- | lib/TypeMapper.php | 175 | ||||
-rw-r--r-- | lib/Utils.php | 302 | ||||
-rw-r--r-- | scripts/copyright_updater.php | 64 |
74 files changed, 16118 insertions, 0 deletions
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/changelog b/debian/changelog index 1c8e431..1c8e431 100644 --- a/changelog +++ b/debian/changelog diff --git a/copyright b/debian/copyright index 47c6691..47c6691 100644 --- a/copyright +++ b/debian/copyright diff --git a/gbp.conf b/debian/gbp.conf index 48d3504..48d3504 100644 --- a/gbp.conf +++ b/debian/gbp.conf diff --git a/source/format b/debian/source/format index 163aaf8..163aaf8 100644 --- a/source/format +++ b/debian/source/format diff --git a/tests/control b/debian/tests/control index 358fac3..358fac3 100644 --- a/tests/control +++ b/debian/tests/control diff --git a/tests/data/foaf.rdf b/debian/tests/data/foaf.rdf index 8d3e311..8d3e311 100644 --- a/tests/data/foaf.rdf +++ b/debian/tests/data/foaf.rdf diff --git a/tests/rapper.php b/debian/tests/rapper.php index 7939398..7939398 100644 --- a/tests/rapper.php +++ b/debian/tests/rapper.php diff --git a/tests/rapper.sh b/debian/tests/rapper.sh index b86157b..b86157b 100644 --- a/tests/rapper.sh +++ b/debian/tests/rapper.sh diff --git a/tests/test.php b/debian/tests/test.php index 59d956b..59d956b 100644 --- a/tests/test.php +++ b/debian/tests/test.php diff --git a/tests/test.sh b/debian/tests/test.sh index d9b8b39..d9b8b39 100644 --- a/tests/test.sh +++ b/debian/tests/test.sh diff --git a/upstream/metadata b/debian/upstream/metadata index 539585a..539585a 100644 --- a/upstream/metadata +++ b/debian/upstream/metadata diff --git a/doap.php b/doap.php new file mode 100644 index 0000000..e27f4ae --- /dev/null +++ b/doap.php @@ -0,0 +1,44 @@ +<?php + require_once __DIR__."/vendor/autoload.php"; + + // Load some properties from the composer file + $composer = json_decode(file_get_contents(__DIR__."/composer.json")); + + // Start building up a RDF graph + $doap = new \EasyRdf\Graph($composer->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2013-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2013-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Sub-class of EasyRdf\Resource that represents an RDF collection (rdf:List) + * + * This class can be used to iterate through a collection of items. + * + * Note that items are numbered from 1 (not 0) for consistency with RDF Containers. + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#date + * @copyright Copyright (c) 2013-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Collection extends Resource implements \ArrayAccess, \Countable, \SeekableIterator +{ + private $position; + private $current; + + /** Create a new collection - do not use this directly + * + * @ignore + */ + public function __construct($uri, $graph) + { + $this->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2013-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2013-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Sub-class of EasyRdf\Resource that represents an RDF container + * (rdf:Alt, rdf:Bag and rdf:Seq) + * + * This class can be used to iterate through a list of items. + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#date + * @copyright Copyright (c) 2013-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Container extends Resource implements \ArrayAccess, \Countable, \SeekableIterator +{ + private $position; + + /** Create a new container - do not use this directly + * + * @ignore + */ + public function __construct($uri, $graph) + { + $this->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * EasyRdf Exception class + * + * All exceptions thrown by EasyRdf are an instance of this class. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Exception extends \Exception +{ + // Comment to make PHP CodeSniffer happy +} diff --git a/lib/Format.php b/lib/Format.php new file mode 100644 index 0000000..58b6b20 --- /dev/null +++ b/lib/Format.php @@ -0,0 +1,735 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Class the represents an RDF file format. + * + * For each format, the name, label, URIs and associated MIME Types are + * stored. A single parser and serialiser can also be registered to each + * format. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Format +{ + private static $formats = array(); + + private $name = array(); + private $label = null; + private $uri = null; + private $mimeTypes = array(); + private $extensions = array(); + private $parserClass = null; + private $serialiserClass = null; + + /** Get a list of format names + * + * @return array An array of formats name + */ + public static function getNames() + { + return array_keys(self::$formats); + } + + /** Get a list of all the registered formats + * + * @return array An array of format objects + */ + public static function getFormats() + { + return self::$formats; + } + + /** Generates an HTTP Accept header string + * + * The string will contain all of the MIME Types that we + * are able to parse. + * + * It is also possible to specify additional MIME types + * in the form array('text/plain' => 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('/<rdf:/i', $short)) { + return self::getFormat('rdfxml'); + } elseif (preg_match('|http://www.w3.org/2005/sparql-results|', $short)) { + return self::getFormat('sparql-xml'); + } elseif (preg_match('/\WRDFa\W/i', $short)) { + return self::getFormat('rdfa'); + } elseif (preg_match('/<!DOCTYPE html|<html/i', $short)) { + # We don't support any other microformats embedded in HTML + return self::getFormat('rdfa'); + } elseif (preg_match('/@prefix\s|@base\s/', $short)) { + return self::getFormat('turtle'); + } elseif (preg_match('/prefix\s|base\s/i', $short)) { + return self::getFormat('turtle'); + } elseif (preg_match('/^\s*<.+> <.+>/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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Container for collection of EasyRdf\Resource objects. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Graph +{ + /** The URI of the graph */ + private $uri = null; + private $parsedUri = null; + + /** Array of resources contained in the graph */ + private $resources = array(); + + private $index = array(); + private $revIndex = array(); + + /** Counter for the number of bnodes */ + private $bNodeCount = 0; + + /** Array of URLs that have been loaded into the graph */ + private $loaded = array(); + + private $maxRedirects = 10; + + + /** + * Constructor + * + * If no URI is given then an unnamed graph is created. + * + * The $data parameter is optional and will be parsed into + * the graph if given. + * + * The data format is optional and should be specified if it + * can't be guessed by EasyRdf. + * + * @param string $uri The URI of the graph + * @param string $data Data for the graph + * @param string $format The document type of the data (e.g. rdfxml) + * + * @return Graph + */ + public function __construct($uri = null, $data = null, $format = null) + { + $this->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 .= "<div style='font-family:arial; font-weight: bold; padding:0.5em; ". + "color: black; background-color:lightgrey;border:dashed 1px grey;'>". + "Graph: ". $this->uri . "</div>\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 []= "<span style='font-size:130%'>→</span> ". + "<span style='text-decoration:none;color:green'>". + htmlentities($pstr) . "</span> ". + "<span style='font-size:130%'>→</span> ". + join(", ", $olist); + } else { + $plist []= " -> $pstr -> " . join(", ", $olist); + } + } + + if ($format == 'html') { + return "<div id='".htmlentities($resource, ENT_QUOTES)."' " . + "style='font-family:arial; padding:0.5em; ". + "background-color:lightgrey;border:dashed 1px grey;'>\n". + "<div>".Utils::dumpResourceValue($resource, $format, 'blue')." ". + "<span style='font-size: 0.8em'>(". + $this->classForResource($resource).")</span></div>\n". + "<div style='padding-left: 3em'>\n". + "<div>".join("</div>\n<div>", $plist)."</div>". + "</div></div>\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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * A class for fetching, saving and deleting graphs to a Graph Store. + * Implementation of the SPARQL 1.1 Graph Store HTTP Protocol. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class GraphStore +{ + /** + * Use to reference default graph of triplestore + */ + const DEFAULT_GRAPH = 'urn:easyrdf:default-graph'; + + /** The address of the GraphStore endpoint */ + private $uri = null; + private $parsedUri = null; + + + /** Create a new SPARQL Graph Store client + * + * @param string $uri The address of the graph store endpoint + */ + public function __construct($uri) + { + $this->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + + +/** + * Static class to set the HTTP client used by EasyRdf + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Http +{ + /** The default HTTP Client object */ + private static $defaultHttpClient = null; + + /** Set the HTTP Client object used to fetch RDF data + * + * @param Http\Client|\Zend\Http\Client $httpClient The new HTTP client object + * + * @throws \InvalidArgumentException + * @return Http\Client|\Zend\Http\Client The new HTTP client object + */ + public static function setDefaultHttpClient($httpClient) + { + if (!is_object($httpClient) or + !($httpClient instanceof \Zend\Http\Client or + $httpClient instanceof Http\Client)) { + throw new \InvalidArgumentException( + '$httpClient should be an object of class Zend\Http\Client or EasyRdf\Http\Client' + ); + } + return self::$defaultHttpClient = $httpClient; + } + + /** Get the HTTP Client object used to fetch RDF data + * + * If no HTTP Client has previously been set, then a new + * default (EasyRdf\Http\Client) client will be created. + * + * @return Http\Client|\Zend\Http\Client The HTTP client object + */ + public static function getDefaultHttpClient() + { + if (!isset(self::$defaultHttpClient)) { + self::$defaultHttpClient = new Http\Client(); + } + return self::$defaultHttpClient; + } +} diff --git a/lib/Http/Client.php b/lib/Http/Client.php new file mode 100644 index 0000000..7b29389 --- /dev/null +++ b/lib/Http/Client.php @@ -0,0 +1,575 @@ +<?php +namespace EasyRdf\Http; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2020 Nicholas J Humfrey. All rights reserved. + * Copyright (c) 2005-2009 Zend Technologies USA Inc. + * + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 2005-2009 Zend Technologies USA Inc. + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\ParsedUri; + +/** + * This class is an implemetation of an HTTP client in PHP. + * It supports basic HTTP 1.0 and 1.1 requests. For a more complete + * implementation try Zend_Http_Client. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Client +{ + /** + * Configuration array, set using the constructor or using ::setConfig() + * + * @var array + */ + private $config = array( + 'maxredirects' => 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 @@ +<?php +namespace EasyRdf\Http; + +class Exception extends \EasyRdf\Exception +{ + private $body; + + public function __construct($message = "", $code = 0, \Exception $previous = null, $body = '') + { + parent::__construct($message, $code, $previous); + $this->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 @@ +<?php +namespace EasyRdf\Http; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2020 Nicholas J Humfrey. + * Copyright (c) 2005-2009 Zend Technologies USA Inc. + * All rights reserved. + * + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; + +/** + * Class that represents an HTTP 1.0 / 1.1 response message. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 2005-2009 Zend Technologies USA Inc. + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Response +{ + + /** + * The HTTP response status code + * + * @var int + */ + private $status; + + /** + * The HTTP response code as string + * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500) + * + * @var string + */ + private $message; + + /** + * The HTTP response headers array + * + * @var array + */ + private $headers = array(); + + /** + * The HTTP response body + * + * @var string + */ + private $body; + + /** + * Constructor. + * + * @param int $status HTTP Status code + * @param array $headers The HTTP response headers + * @param string $body The content of the response + * @param string $version The HTTP Version (1.0 or 1.1) + * @param string $message The HTTP response Message + */ + public function __construct( + $status, + $headers, + $body = null, + $version = '1.1', + $message = null + ) { + $this->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", "<br />") + * + * @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", "<br />") + * + * @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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2013-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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2013-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Functions for comparing two graphs with each other + * + * Based on rdf-isomorphic.rb by Ben Lavender: + * https://github.com/ruby-rdf/rdf-isomorphic + * + * @package EasyRdf + * @copyright Copyright (c) 2013-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Isomorphic +{ + /** + * Check if one graph is isomorphic (equal) to another graph + * + * For example: + * $graphA = EasyRdf\Graph::newAndLoad('http://example.com/a.ttl'); + * $graphB = EasyRdf\Graph::newAndLoad('http://example.com/b.ttl'); + * if (EasyRdf\Isomorphic::isomorphic($graphA, $graphB)) print "Equal!"; + * + * @param Graph $graphA The first graph to be compared + * @param Graph $graphB The second graph to be compared + * + * @return boolean True if the two graphs are isomorphic + */ + public static function isomorphic($graphA, $graphB) + { + return is_array(self::bijectionBetween($graphA, $graphB)); + } + + /** + * Returns an associative array of bnode identifiers representing an isomorphic + * bijection of one EasyRdf\Graph to another EasyRdf\Graph's blank nodes or + * null if a bijection cannot be found. + * + * @param Graph $graphA The first graph to be compared + * @param Graph $graphB The second graph to be compared + * + * @return array|null bnode mapping from $graphA to $graphB + */ + public static function bijectionBetween($graphA, $graphB) + { + $bnodesA = array(); + $bnodesB = array(); + $statementsA = array(); + $statementsB = array(); + + // Quick initial check: are there differing numbers of subjects? + if (self::countSubjects($graphA) != self::countSubjects($graphB)) { + return null; + } + + // Check if all the statements in Graph A exist in Graph B + $groundedStatementsMatch = self::groundedStatementsMatch($graphA, $graphB, $bnodesA, $statementsA); + + if ($groundedStatementsMatch) { + // Check if all the statements in Graph B exist in Graph A + $groundedStatementsMatch = self::groundedStatementsMatch($graphB, $graphA, $bnodesB, $statementsB); + } + + if ($groundedStatementsMatch === false) { + // The grounded statements do not match + return null; + } elseif (count($bnodesA) > 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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Class that represents an RDF Literal + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Literal +{ + /** @ignore a mapping from datatype uri to class name */ + private static $datatypeMap = array(); + + /** @ignore A mapping from class name to datatype URI */ + private static $classMap = array(); + + /** @ignore The string value for this literal */ + protected $value = null; + + /** @ignore The language of the literal (e.g. 'en') */ + protected $lang = null; + + /** @ignore The datatype URI of the literal */ + protected $datatype = null; + + + /** Create a new literal object + * + * PHP values of type bool, int or float, will automatically be converted + * to the corresponding datatype and PHP sub-class. + * + * If a registered datatype is given, then the registered subclass of EasyRdf\Literal + * will instantiated. + * + * Note that literals are not required to have a language or datatype. + * Literals cannot have both a language and a datatype. + * + * @param mixed $value The value of the literal or an associative array + * @param string $lang The natural language of the literal or null (e.g. 'en') + * @param string $datatype The datatype of the literal or null (e.g. 'xsd:integer') + * + * @return self (or subclass of EasyRdf\Literal) + */ + public static function create($value, $lang = null, $datatype = null) + { + if (Utils::isAssociativeArray($value)) { + if (isset($value['xml:lang'])) { + $lang = $value['xml:lang']; + } elseif (isset($value['lang'])) { + $lang = $value['lang']; + } + if (isset($value['datatype'])) { + $datatype = $value['datatype']; + } + $value = isset($value['value']) ? $value['value'] : null; + } + + if (is_null($datatype) or $datatype === '') { + if (is_null($lang) or $lang === '') { + // Automatic datatype selection + $datatype = self::getDatatypeForValue($value); + } + } elseif (is_object($datatype)) { + $datatype = strval($datatype); + } else { + // Expand shortened URIs (qnames) + $datatype = RdfNamespace::expand($datatype); + } + + // Work out what class to use for this datatype + if (isset(self::$datatypeMap[$datatype])) { + $class = self::$datatypeMap[$datatype]; + } else { + $class = 'EasyRdf\Literal'; + } + return new $class($value, $lang, $datatype); + } + + /** Register an RDF datatype with a PHP class name + * + * When parsing registered class will be used whenever the datatype + * is seen. + * + * When serialising a registered class, the mapping will be used to + * set the datatype in the RDF. + * + * Example: + * EasyRdf\Literal::registerDatatype('xsd:dateTime', 'My_DateTime_Class'); + * + * @param string $datatype The RDF datatype (e.g. xsd:dateTime) + * @param string $class The PHP class name (e.g. My_DateTime_Class) + * + * @throws \InvalidArgumentException + */ + public static function setDatatypeMapping($datatype, $class) + { + if (!is_string($datatype) or $datatype == null or $datatype == '') { + throw new \InvalidArgumentException( + "\$datatype should be a string and cannot be null or empty" + ); + } + + if (!is_string($class) or $class == null or $class == '') { + throw new \InvalidArgumentException( + "\$class should be a string and cannot be null or empty" + ); + } + + $datatype = RdfNamespace::expand($datatype); + self::$datatypeMap[$datatype] = $class; + self::$classMap[$class] = $datatype; + } + + /** Remove the mapping between an RDF datatype and a PHP class name + * + * @param string $datatype The RDF datatype (e.g. xsd:dateTime) + * + * @throws \InvalidArgumentException + */ + public static function deleteDatatypeMapping($datatype) + { + if (!is_string($datatype) or $datatype == null or $datatype == '') { + throw new \InvalidArgumentException( + "\$datatype should be a string and cannot be null or empty" + ); + } + + $datatype = RdfNamespace::expand($datatype); + if (isset(self::$datatypeMap[$datatype])) { + $class = self::$datatypeMap[$datatype]; + unset(self::$datatypeMap[$datatype]); + unset(self::$classMap[$class]); + } + } + + /** Get datatype URI for a PHP value. + * + * This static function is intended for internal use. + * Given a PHP value, it will return an XSD datatype + * URI for that value, for example: + * http://www.w3.org/2001/XMLSchema#integer + * + * @param mixed $value + * + * @return string A URI for the datatype of $value. + */ + public static function getDatatypeForValue($value) + { + if (is_float($value)) { + return 'http://www.w3.org/2001/XMLSchema#double'; + } elseif (is_int($value)) { + return 'http://www.w3.org/2001/XMLSchema#integer'; + } elseif (is_bool($value)) { + return 'http://www.w3.org/2001/XMLSchema#boolean'; + } elseif (is_object($value) and $value instanceof \DateTime) { + return 'http://www.w3.org/2001/XMLSchema#dateTime'; + } else { + return null; + } + } + + + + /** Constructor for creating a new literal + * + * @param string $value The value of the literal + * @param string $lang The natural language of the literal or null (e.g. 'en') + * @param string $datatype The datatype of the literal or null (e.g. 'xsd:string') + * + * @return Literal + */ + public function __construct($value, $lang = null, $datatype = null) + { + $this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:boolean + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#boolean + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Boolean extends Literal +{ + /** Constructor for creating a new boolean literal + * + * If the value is not a string, then it will be converted to 'true' or 'false'. + * + * @param mixed $value The value of the literal + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:boolean') + */ + public function __construct($value, $lang = null, $datatype = null) + { + if (!is_string($value)) { + $value = $value ? 'true' : 'false'; + } + parent::__construct($value, null, $datatype); + } + + /** Return the value of the literal cast to a PHP bool + * + * If the value is 'true' or '1' return true, otherwise returns false. + * + * @return bool + */ + public function getValue() + { + return strtolower($this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:date + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#date + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Date extends Literal +{ + /** Constructor for creating a new date literal + * + * If the value is a DateTime object, then it will be converted to the xsd:date format. + * If no value is given or is is null, then the current date is used. + * + * @see \DateTime + * + * @param mixed $value The value of the literal + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:date') + */ + public function __construct($value = null, $lang = null, $datatype = null) + { + // If $value is null, use today's date + if (is_null($value)) { + $value = new \DateTime('today'); + } + + // Convert DateTime object into string + if ($value instanceof \DateTime) { + $value = $value->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:dateTime + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#date + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class DateTime extends Date +{ + /** Constructor for creating a new date and time literal + * + * If the value is a DateTime object, then it will be converted to the xsd:dateTime format. + * If no value is given or is is null, then the current time is used. + * + * @see \DateTime + * + * @param mixed $value The value of the literal + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:dateTime') + */ + public function __construct($value = null, $lang = null, $datatype = null) + { + // If $value is null, use 'now' + if (is_null($value)) { + $value = new \DateTime('now'); + } + + // Convert DateTime objects into string + if ($value instanceof \DateTime) { + $atom = $value->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:decimal + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#decimal + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Decimal extends Literal +{ + /** + * written according to http://www.w3.org/TR/xmlschema-2/#decimal + */ + const DECIMAL_REGEX = '^([+\-]?)(((\d+)?\.(\d+))|((\d+)\.?))$'; + + /** Constructor for creating a new decimal literal + * + * @param double|int|string $value The value of the literal + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:decimal') + * + * @throws \UnexpectedValueException + */ + public function __construct($value, $lang = null, $datatype = null) + { + if (is_string($value)) { + self::validate($value); + } elseif (is_double($value) or is_int($value)) { + $locale_data = localeconv(); + $value = str_replace($locale_data['decimal_point'], '.', strval($value)); + } else { + throw new \UnexpectedValueException('EasyRdf\Literal\Decimal expects int/float/string as value'); + } + + $value = self::canonicalise($value); + + parent::__construct($value, null, $datatype); + } + + /** Return the value of the literal cast to a PHP string + * + * @return string + */ + public function getValue() + { + return strval($this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype rdf:HTML + * + * @package EasyRdf + * @link http://www.w3.org/TR/rdf11-concepts/#section-html + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class HTML extends Literal +{ + /** Constructor for creating a new rdf:HTML literal + * + * @param mixed $value The HTML fragment + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'rdf:HTML') + */ + public function __construct($value, $lang = null, $datatype = null) + { + parent::__construct($value, null, $datatype); + } + + /** Strip the HTML tags from the literal + * + * @link http://php.net/manual/en/function.strip-tags.php + * @param string $allowableTags Optional allowed tag, not be be removed + * + * @return string The literal as plain text + */ + public function stripTags($allowableTags = null) + { + return strip_tags($this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:hexBinary + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#hexBinary + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class HexBinary extends Literal +{ + /** Constructor for creating a new xsd:hexBinary literal + * + * @param mixed $value The value of the literal (already encoded as hexadecimal) + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:hexBinary') + * + * @throws \InvalidArgumentException + */ + public function __construct($value, $lang = null, $datatype = null) + { + // Normalise the canonical representation, as specified here: + // http://www.w3.org/TR/xmlschema-2/#hexBinary-canonical-repr + $value = strtoupper($value); + + // Validate the data + if (preg_match('/[^A-F0-9]/', $value)) { + throw new \InvalidArgumentException( + "Literal of type xsd:hexBinary contains non-hexadecimal characters" + ); + } + + parent::__construct(strtoupper($value), null, 'xsd:hexBinary'); + } + + /** Constructor for creating a new literal object from a binary blob + * + * @param string $binary The binary data + * + * @return self + */ + public static function fromBinary($binary) + { + return new self(bin2hex($binary)); + } + + /** Decode the hexadecimal string into a binary blob + * + * @return string The binary blob + */ + public function toBinary() + { + return pack("H*", $this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype xsd:integer + * + * @package EasyRdf + * @link http://www.w3.org/TR/xmlschema-2/#integer + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Integer extends Literal +{ + /** Constructor for creating a new integer literal + * + * @param mixed $value The value of the literal + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'xsd:integer') + */ + public function __construct($value, $lang = null, $datatype = null) + { + parent::__construct($value, null, $datatype); + } + + /** Return the value of the literal cast to a PHP int + * + * @return int + */ + public function getValue() + { + return (int)$this->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 @@ +<?php +namespace EasyRdf\Literal; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Literal; + +/** + * Class that represents an RDF Literal of datatype rdf:XMLLiteral + * + * @package EasyRdf + * @link http://www.w3.org/TR/REC-rdf-syntax/#section-Syntax-XML-literals + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class XML extends Literal +{ + /** Constructor for creating a new rdf:XMLLiteral literal + * + * @param mixed $value The XML fragment + * @param string $lang Should be null (literals with a datatype can't have a language) + * @param string $datatype Optional datatype (default 'rdf:XMLLiteral') + */ + public function __construct($value, $lang = null, $datatype = null) + { + parent::__construct($value, null, $datatype); + } + + /** Parse the XML literal into a DOMDocument + * + * @link http://php.net/manual/en/domdocument.loadxml.php + * @return \DOMDocument + */ + public function domParse() + { + $dom = new \DOMDocument(); + $dom->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + + +/** + * A RFC3986 compliant URI parser + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + * @link http://www.ietf.org/rfc/rfc3986.txt + */ +class ParsedUri +{ + // For all URIs: + private $scheme = null; + private $fragment = null; + + // For hierarchical URIs: + private $authority = null; + private $path = null; + private $query = null; + + const URI_REGEX = "|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|"; + + /** Constructor for creating a new parsed URI + * + * The $uri parameter can either be a string or an + * associative array with the following keys: + * scheme, authority, path, query, fragment + * + * @param mixed $uri The URI as a string or an array + */ + public function __construct($uri = null) + { + if (is_string($uri)) { + if (preg_match(self::URI_REGEX, $uri, $matches)) { + if (!empty($matches[1])) { + $this->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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2015 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Parent class for the EasyRdf parsers + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Parser +{ + /** Mapping from source to graph bnode identifiers */ + private $bnodeMap = array(); + + /** The current graph to insert triples into */ + /** @var Graph */ + protected $graph = null; + + /** The format of the document currently being parsed */ + protected $format = null; + + /** The base URI for the document currently being parsed */ + protected $baseUri = null; + + + protected $tripleCount = 0; + + /** + * Create a new, unique bnode identifier from a source identifier. + * If the source identifier has previously been seen, the + * same new bnode identifier is returned. + * @ignore + */ + protected function remapBnode($name) + { + if (!isset($this->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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Class to parse RDF using the ARC2 library. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Arc extends RdfPhp +{ + private static $supportedTypes = array( + 'rdfxml' => '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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2013-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * EasyRdf Exception class + * + * All exceptions thrown by EasyRdf are an instance of this class. + * + * @package EasyRdf + * @copyright Copyright (c) 2013-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Exception extends \EasyRdf\Exception +{ + protected $parserLine; + protected $parserColumn; + + public function __construct($message, $line = null, $column = null) + { + $this->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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; + +/** + * A pure-php class to parse RDF/JSON with no dependencies. + * + * https://www.easyrdf.org/docs/rdf-formats-json + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Json extends RdfPhp +{ + private $jsonLastErrorExists = false; + + /** + * Constructor + */ + public function __construct() + { + $this->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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\Parser; +use ML\JsonLD as LD; + +/** + * Class to parse JSON-LD to an EasyRdf\Graph + * + * @package EasyRdf + * @copyright Copyright (c) 2014 Markus Lanthaler + * @author Markus Lanthaler <mail@markus-lanthaler.com> + * @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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\Parser; + +/** + * A pure-php class to parse N-Triples with no dependencies. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Ntriples extends Parser +{ + /** + * Decodes an encoded N-Triples string. Any \-escape sequences are substituted + * with their decoded value. + * + * @param string $str An encoded N-Triples string. + * + * @return string The unencoded string. + **/ + protected function unescapeString($str) + { + if (strpos($str, '\\') === false) { + return $str; + } + + $mappings = array( + 't' => 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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\Utils; + +/** + * Class to parse RDF using the 'rapper' command line tool. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Rapper extends Json +{ + private $rapperCmd = null; + + const MINIMUM_RAPPER_VERSION = '1.4.17'; + + /** + * Constructor + * + * @param string $rapperCmd Optional path to the rapper command to use. + * + * @throws \EasyRdf\Exception + */ + public function __construct($rapperCmd = 'rapper') + { + exec("$rapperCmd --version 2>/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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\Parser; + +/** + * Class to parse RDF with no external dependancies. + * + * https://www.easyrdf.org/docs/rdf-formats-php + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class RdfPhp extends Parser +{ + /** + * Constructor + */ + public function __construct() + { + } + + /** + * Parse RDF/PHP into an EasyRdf\Graph + * + * @param Graph $graph the graph to load the data into + * @param array[] $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) + { + $this->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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2010-2020 Nicholas J Humfrey + * Copyright (c) 2004-2010 Benjamin Nowack (based on ARC2_RDFXMLParser.php) + * + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2010-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\ParsedUri; +use EasyRdf\Parser; + +/** + * A pure-php class to parse RDF/XML. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 2004-2010 Benjamin Nowack (based on ARC2_RDFXMLParser.php) + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class RdfXml extends Parser +{ + private $state; + private $xLang; + private $xBase; + private $xml; + private $rdf; + private $nsp; + private $sStack; + private $sCount; + + /** + * Constructor + */ + public function __construct() + { + } + + /** @ignore */ + protected function init($graph, $base) + { + $this->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 .= '</'.$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.'>'; + } + $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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2012-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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 1997-2006 Aduna (http://www.aduna-software.com/) + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\ParsedUri; +use EasyRdf\Parser; +use EasyRdf\RdfNamespace; + +/** + * Class to parse RDFa 1.1 with no external dependancies. + * + * http://www.w3.org/TR/rdfa-core/ + * + * @package EasyRdf + * @copyright Copyright (c) 2012-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Rdfa extends Parser +{ + const XML_NS = 'http://www.w3.org/XML/1998/namespace'; + const RDF_XML_LITERAL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral'; + const TERM_REGEXP = '/^([a-zA-Z_])([0-9a-zA-Z_\.-]*)$/'; + + public $debug = false; + + /** + * Constructor + */ + public function __construct() + { + } + + protected function addTriple($resource, $property, $value) + { + if ($this->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 @@ +<?php +namespace EasyRdf\Parser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2020 Nicholas J Humfrey. + * Copyright (c) 1997-2013 Aduna (http://www.aduna-software.com/) + * All rights reserved. + * + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 1997-2006 Aduna (http://www.aduna-software.com/) + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Graph; +use EasyRdf\ParsedUri; +use EasyRdf\RdfNamespace; + +/** + * Class to parse Turtle with no external dependencies. + * + * It is a translation from Java to PHP of the Sesame Turtle Parser: + * http://bit.ly/TurtleParser + * + * Lasted updated against version: + * ecda6a15a200a2fc6a062e2e43081257c3ccd4e6 (Mon Jul 29 12:05:58 2013) + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * Copyright (c) 1997-2013 Aduna (http://www.aduna-software.com/) + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Turtle extends Ntriples +{ + protected $data; + protected $namespaces; + protected $subject; + protected $predicate; + protected $object; + + protected $line; + protected $column; + + protected $bytePos; + protected $dataLength; + + /** + * Constructor + */ + public function __construct() + { + } + + /** + * Parse Turtle into an 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 != 'turtle') { + throw new \EasyRdf\Exception( + "EasyRdf\\Parser\\Turtle does not support: {$format}" + ); + } + + $this->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. <foo://bar> + 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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * A namespace registry and manipulation class. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class RdfNamespace +{ + /** Namespace registry + * + * List of default namespaces come from: + * - http://www.w3.org/2011/rdfa-context/rdfa-1.1 + * + * With a few extras added. + * + */ + private static $initial_namespaces = array( + 'as' => '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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2015 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Class that represents an RDF resource + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Resource implements \ArrayAccess +{ + /** The URI for this resource */ + protected $uri = null; + + /** The Graph that this resource belongs to */ + protected $graph = null; + + + /** Constructor + * + * * Please do not call new EasyRdf\Resource() directly * + * + * To create a new resource use the get method in a graph: + * $resource = $graph->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 = "<a"; + foreach ($options as $key => $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)."</a>"; + + 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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2016 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2016 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Parent class for the EasyRdf serialiser + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2016 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +abstract class Serialiser +{ + protected $prefixes = array(); + + /** + * Keep track of the prefixes used while serialising + * @ignore + */ + protected function addPrefix($qname) + { + list ($prefix) = explode(':', $qname); + $this->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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2016 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2016 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Format; +use EasyRdf\Graph; + +/** + * Class to serialise RDF using the ARC2 library. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2016 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Arc extends RdfPhp +{ + private static $supportedTypes = array( + 'rdfxml' => '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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2012-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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\RdfNamespace; +use EasyRdf\Resource; +use EasyRdf\Serialiser; +use EasyRdf\Utils; + +/** + * Class to serialise an EasyRdf\Graph to GraphViz + * + * Depends upon the GraphViz 'dot' command line tools to render images. + * + * See http://www.graphviz.org/ for more information. + * + * @package EasyRdf + * @copyright Copyright (c) 2012-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class GraphViz extends Serialiser +{ + private $dotCommand = 'dot'; + private $useLabels = false; + private $onlyLabelled = false; + private $attributes = array('charset' => '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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; + +/** + * Class to serialise an EasyRdf\Graph to RDF/JSON + * with no external dependencies. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Json extends RdfPhp +{ + /** + * Serialise an EasyRdf\Graph into a to RDF/JSON document. + * + * https://www.easyrdf.org/docs/rdf-formats-json + * + * @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 != 'json') { + throw new Exception( + "EasyRdf\\Serialiser\\Json does not support: {$format}" + ); + } + + return json_encode(parent::serialise($graph, 'php')); + } +} diff --git a/lib/Serialiser/JsonLd.php b/lib/Serialiser/JsonLd.php new file mode 100644 index 0000000..4961e91 --- /dev/null +++ b/lib/Serialiser/JsonLd.php @@ -0,0 +1,148 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Serialiser; + +use ML\JsonLD as LD; + +/** + * Class to serialise an EasyRdf\Graph to JSON-LD + * + * @package EasyRdf + * @copyright Copyright (c) 2013 Alexey Zakhlestin + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class JsonLd extends Serialiser +{ + public function __construct() + { + if (!class_exists('\ML\JsonLD\JsonLD')) { + throw new \LogicException('Please install "ml/json-ld" dependency to use JSON-LD serialisation'); + } + } + + + /** + * Serialise an EasyRdf\Graph into a JSON-LD document. + * + * @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 != 'jsonld') { + throw new Exception(__CLASS__.' does not support: '.$format); + } + + + $ld_graph = new LD\Graph(); + $nodes = array(); // cache for id-to-node association + + foreach ($graph->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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Serialiser; + +/** + * Class to serialise an EasyRdf\Graph to N-Triples + * with no external dependencies. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Ntriples extends Serialiser +{ + private $escChars = array(); // Character encoding cache + + /** + * @ignore + */ + protected function escapeString($str) + { + $result = ''; + $strLen = mb_strlen($str, "UTF-8"); + + for ($i = 0; $i < $strLen; $i++) { + $c = mb_substr($str, $i, 1, "UTF-8"); + + if (!isset($this->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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Utils; + +/** + * Class to serialise an EasyRdf\Graph to RDF + * using the 'rapper' command line tool. + * + * Note: the built-in N-Triples serialiser is used to pass data to Rapper. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Rapper extends Ntriples +{ + private $rapperCmd = null; + + /** + * Constructor + * + * @param string $rapperCmd Optional path to the rapper command to use. + * + * @throws \EasyRdf\Exception + */ + public function __construct($rapperCmd = 'rapper') + { + exec("$rapperCmd --version 2>/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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Serialiser; + +/** + * Class to serialise an EasyRdf\Graph to RDF/PHP + * with no external dependencies. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class RdfPhp extends Serialiser +{ + /** + * Method to serialise an EasyRdf\Graph to RDF/PHP + * + * https://www.easyrdf.org/docs/rdf-formats-php + * + * @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 != 'php') { + throw new Exception( + __CLASS__." does not support: $format" + ); + } + + // Graph is already stored as RDF/PHP resource-centric array internally within the EasyRdf\Graph object + return $graph->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 @@ +<?php +namespace EasyRdf\Serialiser; + + /** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Container; +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Literal; +use EasyRdf\RdfNamespace; +use EasyRdf\Resource; +use EasyRdf\Serialiser; + +/** + * Class to serialise an EasyRdf\Graph to RDF/XML + * with no external dependencies. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class RdfXml extends Serialiser +{ + private $outputtedResources = array(); + + /** A constant for the RDF Type property URI */ + const RDF_XML_LITERAL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral'; + + /** + * Protected method to serialise an object node into an XML object + * @ignore + */ + protected function rdfxmlObject($property, $obj, $depth) + { + $indent = str_repeat(' ', $depth); + + if ($property[0] === ':') { + $property = substr($property, 1); + } + + if (is_object($obj) and $obj instanceof Resource) { + $pcount = count($obj->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</$property>\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}</{$property}>\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</$type>\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 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n". + "<rdf:RDF". $namespaceStr . ">\n" . $xml . "\n</rdf:RDF>\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 @@ +<?php +namespace EasyRdf\Serialiser; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Collection; +use EasyRdf\Exception; +use EasyRdf\Graph; +use EasyRdf\Literal; +use EasyRdf\RdfNamespace; +use EasyRdf\Resource; +use EasyRdf\Serialiser; + +/** + * Class to serialise an EasyRdf\Graph to Turtle + * with no external dependencies. + * + * http://www.w3.org/TR/turtle/ + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Turtle extends Serialiser +{ + private $outputtedBnodes = array(); + + /** + * Given a IRI string, escape and enclose in angle brackets. + * + * @param string $resourceIri + * + * @return string + */ + public static function escapeIri($resourceIri) + { + $escapedIri = str_replace('>', '\\>', $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 @@ +<?php +namespace EasyRdf\Sparql; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2015 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Format; +use EasyRdf\Graph; +use EasyRdf\Http; +use EasyRdf\RdfNamespace; +use EasyRdf\Utils; + +/** + * Class for making SPARQL queries using the SPARQL 1.1 Protocol + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Client +{ + /** The query/read address of the SPARQL Endpoint */ + private $queryUri = null; + + private $queryUri_has_params = false; + + /** The update/write address of the SPARQL Endpoint */ + private $updateUri = null; + + /** Create a new SPARQL endpoint client + * + * If the query and update endpoints are the same, then you + * only need to give a single URI. + * + * @param string $queryUri The address of the SPARQL Query Endpoint + * @param string $updateUri Optional address of the SPARQL Update Endpoint + */ + public function __construct($queryUri, $updateUri = null) + { + $this->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 @@ +<?php +namespace EasyRdf\Sparql; + +/** + * EasyRdf + * + * 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +use EasyRdf\Exception; +use EasyRdf\Literal; +use EasyRdf\Resource; + +/** + * Class for returned for SPARQL SELECT and ASK query responses. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2020 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Result extends \ArrayIterator +{ + private $type = null; + private $boolean = null; + + private $fields = array(); + + /** A constant for the SPARQL Query Results XML Format namespace */ + const SPARQL_XML_RESULTS_NS = 'http://www.w3.org/2005/sparql-results#'; + + /** Create a new SPARQL Result object + * + * You should not normally need to create a SPARQL result + * object directly - it will be constructed automatically + * for you by EasyRdf\Sparql\_Client. + * + * @param string $data The SPARQL result body + * @param string $mimeType The MIME type of the result + * + * @throws \EasyRdf\Exception + */ + public function __construct($data, $mimeType) + { + if ($mimeType == 'application/sparql-results+xml') { + $this->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 .= "<table class='sparql-results' style='border-collapse:collapse'>"; + $result .= "<tr>"; + foreach ($this->fields as $field) { + $result .= "<th style='border:solid 1px #000;padding:4px;". + "vertical-align:top;background-color:#eee;'>". + "?$field</th>"; + } + $result .= "</tr>"; + foreach ($this as $row) { + $result .= "<tr>"; + foreach ($this->fields as $field) { + if (isset($row->$field)) { + $result .= "<td style='border:solid 1px #000;padding:4px;". + "vertical-align:top'>". + $row->$field->dumpValue($format)."</td>"; + } else { + $result .= "<td> </td>"; + } + } + $result .= "</tr>"; + } + $result .= "</table>"; + } 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 "<p>Result: <span style='font-weight:bold'>$str</span></p>"; + } 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 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2015 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + +/** + * Class to map between RDF Types and PHP Classes + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2015 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class TypeMapper +{ + /** The type map registry */ + private static $map = array(); + + /** Default resource class */ + private static $defaultResourceClass = 'EasyRdf\Resource'; + + /** Get the registered class for an RDF type + * + * If a type is not registered, then this method will return null. + * + * @param string $type The RDF type (e.g. foaf:Person) + * + * @throws \InvalidArgumentException + * @return string The class name (e.g. Model_Foaf_Name) + */ + public static function get($type) + { + if (!is_string($type) or $type == null or $type == '') { + throw new \InvalidArgumentException( + "\$type should be a string and cannot be null or empty" + ); + } + + $type = RdfNamespace::expand($type); + if (array_key_exists($type, self::$map)) { + return self::$map[$type]; + } else { + return null; + } + } + + /** Register an RDF type with a PHP Class name + * + * @param string $type The RDF type (e.g. foaf:Person) + * @param string $class The PHP class name (e.g. Model_Foaf_Name) + * + * @throws \InvalidArgumentException + * @return string The PHP class name + */ + public static function set($type, $class) + { + if (!is_string($type) or $type == null or $type == '') { + throw new \InvalidArgumentException( + "\$type should be a string and cannot be null or empty" + ); + } + + if (!is_string($class) or $class == null or $class == '') { + throw new \InvalidArgumentException( + "\$class should be a string and cannot be null or empty" + ); + } + + $type = RdfNamespace::expand($type); + return self::$map[$type] = $class; + } + + /** + * Delete an existing RDF type mapping. + * + * @param string $type The RDF type (e.g. foaf:Person) + * + * @throws \InvalidArgumentException + */ + public static function delete($type) + { + if (!is_string($type) or $type == null or $type == '') { + throw new \InvalidArgumentException( + "\$type should be a string and cannot be null or empty" + ); + } + + $type = RdfNamespace::expand($type); + if (isset(self::$map[$type])) { + unset(self::$map[$type]); + } + } + + /** + * @return string The default Resource class + */ + public static function getDefaultResourceClass() + { + return self::$defaultResourceClass; + } + + /** + * Sets the default resource class + * + * @param string $class The resource full class name (e.g. \MyCompany\Resource) + * + * @throws \InvalidArgumentException + * @return string The default Resource class + */ + public static function setDefaultResourceClass($class) + { + if (!is_string($class) or $class == null or $class == '') { + throw new \InvalidArgumentException( + "\$class should be a string and cannot be null or empty" + ); + } + + if (!class_exists($class)) { + throw new \InvalidArgumentException( + "Given class should be an existing class" + ); + } + + $ancestors = class_parents($class); + if (($class != 'EasyRdf\Resource') && (empty($ancestors) || !in_array('EasyRdf\Resource', $ancestors))) { + throw new \InvalidArgumentException( + "Given class should have EasyRdf\\Resource as an ancestor" + ); + } + + return self::$defaultResourceClass = $class; + } +} + + +/* + Register default set of mapped types +*/ + +TypeMapper::set('rdf:Alt', 'EasyRdf\Container'); +TypeMapper::set('rdf:Bag', 'EasyRdf\Container'); +TypeMapper::set('rdf:List', 'EasyRdf\Collection'); +TypeMapper::set('rdf:Seq', 'EasyRdf\Container'); diff --git a/lib/Utils.php b/lib/Utils.php new file mode 100644 index 0000000..5fcd7e5 --- /dev/null +++ b/lib/Utils.php @@ -0,0 +1,302 @@ +<?php +namespace EasyRdf; + +/** + * EasyRdf + * + * LICENSE + * + * Copyright (c) 2009-2014 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: + * 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 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. + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ + + +/** + * Class containing static utility functions + * + * @package EasyRdf + * @copyright Copyright (c) 2009-2014 Nicholas J Humfrey + * @license https://www.opensource.org/licenses/bsd-license.php + */ +class Utils +{ + + /** + * Convert a string into CamelCase + * + * A capital letter is inserted for any non-letter (including userscore). + * For example: + * 'hello world' becomes HelloWorld + * 'rss-tag-soup' becomes RssTagSoup + * 'FOO//BAR' becomes FooBar + * + * @param string $str The input string + * + * @return string The input string converted to CamelCase + */ + public static function camelise($str) + { + $cc = ''; + foreach (preg_split('/[\W_]+/', $str) as $part) { + $cc .= ucfirst(strtolower($part)); + } + return $cc; + } + + /** + * Check if something is an associative array + * + * Note: this method only checks the key of the first value in the array. + * + * @param mixed $param The variable to check + * + * @return bool true if the variable is an associative array + */ + public static function isAssociativeArray($param) + { + if (is_array($param)) { + $keys = array_keys($param); + if ($keys[0] === 0) { + return false; + } else { + return true; + } + } else { + return false; + } + } + + /** + * Remove the fragment from a URI (if it has one) + * + * @param mixed $uri A URI + * + * @return string The same URI with the fragment removed + */ + public static function removeFragmentFromUri($uri) + { + $pos = strpos($uri, '#'); + if ($pos === false) { + return $uri; + } else { + return substr($uri, 0, $pos); + } + } + + /** Return pretty-print view of a resource URI + * + * This method is mainly intended for internal use and is used by + * EasyRdf\Graph and EasyRdf\Sparql\Result to format a resource + * for display. + * + * @param mixed $resource An EasyRdf\Resource 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 dumpResourceValue($resource, $format = 'html', $color = 'blue') + { + if (!preg_match('/^#?[-\w]+$/', $color)) { + throw new \InvalidArgumentException( + "\$color must be a legal color code or name" + ); + } + + if (is_object($resource)) { + $resource = strval($resource); + } elseif (is_array($resource)) { + $resource = $resource['value']; + } + + $short = RdfNamespace::shorten($resource); + if ($format == 'html') { + $escaped = htmlentities($resource, ENT_QUOTES); + if (substr($resource, 0, 2) == '_:') { + $href = '#' . $escaped; + } else { + $href = $escaped; + } + if ($short) { + return "<a href='$href' style='text-decoration:none;color:$color'>$short</a>"; + } else { + return "<a href='$href' style='text-decoration:none;color:$color'>$escaped</a>"; + } + } 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 "<span style='color:$color'>". + htmlentities($text, ENT_COMPAT, "UTF-8"). + "</span>"; + } 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 @@ +<?php + +$ROOT = realpath(__DIR__ . '/..'); + +function process_file($path) { + $year = date('Y', filemtime($path)); + $contents = file_get_contents($path); + + $copy_statements = 0; + $output = ''; + foreach (preg_split("/[\r\n]/", $contents) as $line) { + if (preg_match("/^(.+)Copyright\s+\(c\)\s+(\d+)-?(\d*) (Nicholas.+)$/", $line, $m)) { + $copy_statements++; + + if ($m[2] != $year and $m[3] != $year) { + // Change the line + $line = "$m[1]Copyright (c) $m[2]-$year $m[4]"; + } + } + + // Remove trailing whitespace + $line = rtrim($line); + $output .= "$line\n"; + } + + // Remove surplus line endings + while (substr($output, -2) == "\n\n") { + $output = substr($output, 0, -1); + } + + if ($copy_statements == 0) { + print "Warning: $path does not contain any copyright statements\n"; + } else { + file_put_contents($path, $output); + } +} + + +function process_directory($path) { + $dir = opendir($path); + + while ($file = readdir($dir)) { + if (substr($file, 0, 1) == '.') { + continue; + } + + $filepath = $path . '/' . $file; + if (is_dir($filepath)) { + process_directory($filepath); + } elseif (is_file($filepath)) { + if (substr($file, -4) == '.php') { + process_file($filepath); + } + } else { + print "Unknown type: $filepath\n"; + } + } + + closedir($dir); +} + +process_directory($ROOT . '/examples'); +process_directory($ROOT . '/lib'); +process_directory($ROOT . '/test'); |