summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml22
-rw-r--r--CHANGELOG.md393
-rw-r--r--CODE_OF_CONDUCT.md130
-rw-r--r--DEVELOPER.md31
-rw-r--r--LICENSE.md420
-rw-r--r--README.md126
-rw-r--r--composer.json49
-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-xdebian/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.php44
-rw-r--r--lib/Collection.php338
-rw-r--r--lib/Container.php234
-rw-r--r--lib/Exception.php51
-rw-r--r--lib/Format.php735
-rw-r--r--lib/Graph.php1753
-rw-r--r--lib/GraphStore.php312
-rw-r--r--lib/Http.php85
-rw-r--r--lib/Http/Client.php575
-rw-r--r--lib/Http/Exception.php18
-rw-r--r--lib/Http/Response.php431
-rw-r--r--lib/Isomorphic.php437
-rw-r--r--lib/Literal.php342
-rw-r--r--lib/Literal/Boolean.php94
-rw-r--r--lib/Literal/Date.php140
-rw-r--r--lib/Literal/DateTime.php119
-rw-r--r--lib/Literal/Decimal.php127
-rw-r--r--lib/Literal/HTML.php72
-rw-r--r--lib/Literal/HexBinary.php93
-rw-r--r--lib/Literal/Integer.php69
-rw-r--r--lib/Literal/XML.php72
-rw-r--r--lib/ParsedUri.php340
-rw-r--r--lib/Parser.php154
-rw-r--r--lib/Parser/Arc.php99
-rw-r--r--lib/Parser/Exception.php77
-rw-r--r--lib/Parser/Json.php158
-rw-r--r--lib/Parser/JsonLd.php127
-rw-r--r--lib/Parser/Ntriples.php218
-rw-r--r--lib/Parser/Rapper.php107
-rw-r--r--lib/Parser/RdfPhp.php134
-rw-r--r--lib/Parser/RdfXml.php815
-rw-r--r--lib/Parser/Rdfa.php728
-rw-r--r--lib/Parser/Turtle.php1362
-rw-r--r--lib/RdfNamespace.php443
-rw-r--r--lib/Resource.php828
-rw-r--r--lib/Serialiser.php108
-rw-r--r--lib/Serialiser/Arc.php105
-rw-r--r--lib/Serialiser/GraphViz.php396
-rw-r--r--lib/Serialiser/Json.php75
-rw-r--r--lib/Serialiser/JsonLd.php148
-rw-r--r--lib/Serialiser/Ntriples.php228
-rw-r--r--lib/Serialiser/Rapper.php108
-rw-r--r--lib/Serialiser/RdfPhp.php77
-rw-r--r--lib/Serialiser/RdfXml.php256
-rw-r--r--lib/Serialiser/Turtle.php389
-rw-r--r--lib/Sparql/Client.php395
-rw-r--r--lib/Sparql/Result.php390
-rw-r--r--lib/TypeMapper.php175
-rw-r--r--lib/Utils.php302
-rw-r--r--scripts/copyright_updater.php64
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/clean b/debian/clean
index 6d47d7c..6d47d7c 100644
--- a/clean
+++ b/debian/clean
diff --git a/control b/debian/control
index 8a9c288..8a9c288 100644
--- a/control
+++ b/debian/control
diff --git a/copyright b/debian/copyright
index 47c6691..47c6691 100644
--- a/copyright
+++ b/debian/copyright
diff --git a/docs b/debian/docs
index b43bf86..b43bf86 100644
--- a/docs
+++ b/debian/docs
diff --git a/gbp.conf b/debian/gbp.conf
index 48d3504..48d3504 100644
--- a/gbp.conf
+++ b/debian/gbp.conf
diff --git a/install b/debian/install
index 71c4804..71c4804 100644
--- a/install
+++ b/debian/install
diff --git a/rules b/debian/rules
index b158cdd..b158cdd 100755
--- a/rules
+++ b/debian/rules
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/watch b/debian/watch
index f9c9755..f9c9755 100644
--- a/watch
+++ b/debian/watch
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%'>&rarr;</span> ".
+ "<span style='text-decoration:none;color:green'>".
+ htmlentities($pstr) . "</span> ".
+ "<span style='font-size:130%'>&rarr;</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>&nbsp;</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');