pax_global_header00006660000000000000000000000064143152554350014521gustar00rootroot0000000000000052 comment=537e68e87a6bce23e57c575cd5dcac1f67ce25d8 JsonLD-1.2.1/000077500000000000000000000000001431525543500126535ustar00rootroot00000000000000JsonLD-1.2.1/.devcontainer/000077500000000000000000000000001431525543500154125ustar00rootroot00000000000000JsonLD-1.2.1/.devcontainer/Dockerfile000066400000000000000000000021661431525543500174110ustar00rootroot00000000000000# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/php/.devcontainer/base.Dockerfile # [Choice] PHP version (use -bullseye variants on local arm64/Apple Silicon): 8, 8.1, 8.0, 7, 7.4, 7.3, 8-bullseye, 8.1-bullseye, 8.0-bullseye, 7-bullseye, 7.4-bullseye, 7.3-bullseye, 8-buster, 8.1-buster, 8.0-buster, 7-buster, 7.4-buster ARG VARIANT="8.1-apache-bullseye" FROM mcr.microsoft.com/vscode/devcontainers/php:0-${VARIANT} # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # Enables Xdebug code coverage analysis to generate code coverage reports, mainly in combination with PHPUnit. RUN echo "xdebug.mode = coverage" >> /usr/local/etc/php/conf.d/xdebug.ini \ && echo "xdebug.start_with_request = trigger" >> /usr/local/etc/php/conf.d/xdebug.ini # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 JsonLD-1.2.1/.devcontainer/devcontainer.json000066400000000000000000000027641431525543500207770ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/php { "name": "PHP", "build": { "dockerfile": "Dockerfile", "args": { // Update VARIANT to pick a PHP version: 8, 8.1, 8.0, 7, 7.4 // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local on arm64/Apple Silicon. "VARIANT": "7.4", "NODE_VERSION": "lts/*" } }, // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { // Set *default* container specific settings.json values on container create. "settings": { "php.validate.executablePath": "/usr/local/bin/php", "debug.console.collapseIdenticalLines": false }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "xdebug.php-debug", "bmewburn.vscode-intelephense-client", "mrmlnc.vscode-apache" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [8080], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "sudo chmod a+x \"$(pwd)\" && sudo rm -rf /var/www/html && sudo ln -s \"$(pwd)\" /var/www/html" // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "git": "os-provided" } } JsonLD-1.2.1/.github/000077500000000000000000000000001431525543500142135ustar00rootroot00000000000000JsonLD-1.2.1/.github/workflows/000077500000000000000000000000001431525543500162505ustar00rootroot00000000000000JsonLD-1.2.1/.github/workflows/ci.yaml000066400000000000000000000016711431525543500175340ustar00rootroot00000000000000name: Continuous integration on: push: branches: [ master ] pull_request: branches: [ master ] jobs: ci: runs-on: ubuntu-latest env: extensions: intl, mbstring, xdebug strategy: matrix: php-version: - "5.3" - "5.4" - "5.5" - "5.6" - "7.0" - "7.1" - "7.2" - "7.3" - "7.4" steps: - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" extensions: "${{ env.extensions }}" ini-values: "memory_limit=-1, error_reporting=E_ALL, display_errors=On" coverage: xdebug tools: composer - name: Checkout code uses: actions/checkout@v3 - name: Download dependencies run: composer update --no-interaction --no-progress - name: Run tests run: vendor/bin/phpunit JsonLD-1.2.1/.gitignore000066400000000000000000000000701431525543500146400ustar00rootroot00000000000000/vendor /composer.lock /phpunit.xml /earl-report.jsonld JsonLD-1.2.1/.vscode/000077500000000000000000000000001431525543500142145ustar00rootroot00000000000000JsonLD-1.2.1/.vscode/launch.json000066400000000000000000000013631431525543500163640ustar00rootroot00000000000000{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Listen for Xdebug", "type": "php", "request": "launch", "port": 9000 }, { "name": "Run tests", "type": "php", "request": "launch", "program": "${workspaceFolder}/vendor/bin/phpunit", "cwd": "${workspaceFolder}", "port": 9000, "runtimeArgs": [ "-dxdebug.start_with_request=yes" ], "env": { "XDEBUG_MODE": "debug,develop,coverage", "XDEBUG_CONFIG": "client_port=${port}" } } ] } JsonLD-1.2.1/DefaultDocumentFactory.php000066400000000000000000000010531431525543500177760ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * DefaultDocumentFactory creates new Documents * * @see Document * * @author Markus Lanthaler */ class DefaultDocumentFactory implements DocumentFactoryInterface { /** * {@inheritdoc} */ public function createDocument($iri = null) { return new Document($iri); } } JsonLD-1.2.1/Document.php000066400000000000000000000115131431525543500151430ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; use ML\IRI\IRI; /** * A Document represents a JSON-LD document. * * Named graphs are not supported yet. * * @author Markus Lanthaler */ class Document implements DocumentInterface, JsonLdSerializable { /** * @var IRI The document's IRI */ protected $iri = null; /** * @var GraphInterface The default graph */ protected $defaultGraph = null; /** * @var array An associative array holding all named graphs in the document */ protected $namedGraphs = array(); /** * Parses a JSON-LD document and returns it as a Document * * The document can be supplied directly as a string or by passing a * file path or an IRI. * * Usage: * * $document = Document::load('document.jsonld'); * * * Please note that currently all data is merged into the * default graph, named graphs are not supported yet! * * It is possible to configure the processing by setting the options * parameter accordingly. Available options are: * * - base The base IRI of the input document. * * @param string|array|JsonObject $document The JSON-LD document to process. * @param null|array|JsonObject $options Options to configure the processing. * * @return Document The parsed JSON-LD document. * * @throws ParseException If the JSON-LD input document is invalid. */ public static function load($document, $options = null) { return JsonLD::getDocument($document, $options); } /** * Constructor * * @param null|string|IRI $iri The document's IRI */ public function __construct($iri = null) { $this->iri = new IRI($iri); $this->defaultGraph = new Graph($this); } /** * {@inheritdoc} */ public function setIri($iri) { $this->iri = new IRI($iri); return $this; } /** * {@inheritdoc} */ public function getIri($asObject = false) { return ($asObject) ? $this->iri : (string) $this->iri; } /** * {@inheritdoc} */ public function createGraph($name) { $name = (string) $this->iri->resolve($name); if (isset($this->namedGraphs[$name])) { return $this->namedGraphs[$name]; } return $this->namedGraphs[$name] = new Graph($this, $name); } /** * {@inheritdoc} */ public function getGraph($name = null) { if (null === $name) { return $this->defaultGraph; } $name = (string) $this->iri->resolve($name); return isset($this->namedGraphs[$name]) ? $this->namedGraphs[$name] : null; } /** * {@inheritdoc} */ public function getGraphNames() { return array_keys($this->namedGraphs); } /** * {@inheritdoc} */ public function containsGraph($name) { $name = (string) $this->iri->resolve($name); return isset($this->namedGraphs[$name]); } /** * {@inheritdoc} */ public function removeGraph($graph = null) { // The default graph can't be "removed", it can just be reset if (null === $graph) { $this->defaultGraph = new Graph($this); return $this; } if ($graph instanceof GraphInterface) { foreach ($this->namedGraphs as $n => $g) { if ($g === $graph) { $name = $n; break; } } } else { $name = (string) $this->iri->resolve($graph); } if (isset($this->namedGraphs[$name])) { if ($this->namedGraphs[$name]->getDocument() === $this) { $this->namedGraphs[$name]->removeFromDocument(); } unset($this->namedGraphs[$name]); } return $this; } /** * {@inheritdoc} */ public function toJsonLd($useNativeTypes = true) { $defGraph = $this->defaultGraph->toJsonLd($useNativeTypes); if (0 === count($this->namedGraphs)) { return $defGraph; } foreach ($this->namedGraphs as $graphName => $graph) { $namedGraph = new JsonObject(); $namedGraph->{'@id'} = $graphName; $namedGraph->{'@graph'} = $graph->toJsonLd($useNativeTypes); $defGraph[] = $namedGraph; } $document = new JsonObject(); $document->{'@graph'} = $defGraph; return array($document); } } JsonLD-1.2.1/DocumentFactoryInterface.php000066400000000000000000000011601431525543500203110ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * Interface for factories to create DocumentInterface objects * * @see DocumentInterface * * @author Markus Lanthaler */ interface DocumentFactoryInterface { /** * Creates a new document * * @param null|string $iri The document's IRI. * * @return DocumentInterface The document. */ public function createDocument($iri = null); } JsonLD-1.2.1/DocumentInterface.php000066400000000000000000000045231431525543500167670ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\IRI\IRI; /** * JSON-LD document interface * * @author Markus Lanthaler */ interface DocumentInterface { /** * Set the document's IRI * * @param string|IRI The IRI. * * @return self */ public function setIri($iri); /** * Get the document's IRI * * @param boolean $asObject If set to true, the return value will be an * {@link IRI} object; otherwise a string. * * @return string|IRI The document's IRI (might be empty). */ public function getIri($asObject = false); /** * Creates a new graph which is linked to this document * * If there exists already a graph with the passed name in the document, * that graph will be returned instead of creating a new one. * * @param string|IRI $name The graph's name. * * @return GraphInterface The newly created graph. */ public function createGraph($name); /** * Get a graph by name * * @param null|string $name The name of the graph to retrieve. If null * is passed, the default will be returned. * * @return GraphInterface|null Returns the graph if found; null otherwise. */ public function getGraph($name = null); /** * Get graph names * * @return string[] Returns the names of all graphs in the document. */ public function getGraphNames(); /** * Check whether the document contains a graph with the specified name * * @param string $name The graph name. * * @return bool Returns true if the document contains a graph with the * specified name; false otherwise. */ public function containsGraph($name); /** * Removes a graph from the document * * @param null|string|GraphInterface $graph The graph (or its name) to * remove. If null is passed, * the default will be reset. * * @return self */ public function removeGraph($graph = null); } JsonLD-1.2.1/DocumentLoaderInterface.php000066400000000000000000000012461431525543500201150ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\JsonLD\Exception\JsonLdException; /** * Interface for (remote) document loaders * * @author Markus Lanthaler */ interface DocumentLoaderInterface { /** * Load a (remote) document or context * * @param string $url The URL or path of the document to load. * * @return RemoteDocument The loaded document. * * @throws JsonLdException */ public function loadDocument($url); } JsonLD-1.2.1/Exception/000077500000000000000000000000001431525543500146115ustar00rootroot00000000000000JsonLD-1.2.1/Exception/InvalidQuadException.php000066400000000000000000000023071431525543500214040ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Exception; use ML\JsonLD\Quad; /** * Exception that is thrown when an invalid quad is detected. * * @author Markus Lanthaler */ class InvalidQuadException extends \RuntimeException { /** * The quad that triggered this exception * * @var Quad */ private $quad; /** * Constructor. * * @param string $message The error message * @param Quad $quad The quad * @param null|\Exception $previous The previous exception */ public function __construct($message, $quad, \Exception $previous = null) { $this->quad = $quad; parent::__construct($this->message, 0, $previous); } /** * Gets the quad * * @return Quad The quad. */ public function getQuad() { return $this->quad; } /** * Sets the quad * * @param Quad $quad The quad. */ public function setQuad($quad) { $this->quad = $quad; } } JsonLD-1.2.1/Exception/JsonLdException.php000066400000000000000000000210771431525543500204010ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Exception; use ML\JsonLD\JsonLD; /** * Exception class thrown when an error occurs during parsing. * * @author Markus Lanthaler */ class JsonLdException extends \RuntimeException { /** * An unspecified error code (none was standardized yet) */ const UNSPECIFIED = 'unknown'; /** * The document could not be loaded or parsed as JSON. */ const LOADING_DOCUMENT_FAILED = "loading document failed"; /** * A list of lists was detected. List of lists are not supported in * this version of JSON-LD due to the algorithmic complexity. */ const LIST_OF_LISTS = "list of lists"; /** * An @index member was encountered whose value was not a string. */ const INVALID_INDEX_VALUE = "invalid @index value"; /** * Multiple conflicting indexes have been found for the same node. */ const CONFLICTING_INDEXES = "conflicting indexes"; /** * An @id member was encountered whose value was not a string. */ const INVALID_ID_VALUE = "invalid @id value"; /** * In invalid local context was detected. */ const INVALID_LOCAL_CONTEXT = "invalid local context"; /** * Multiple HTTP Link Headers [RFC5988] using th * http://www.w3.org/ns/json-ld#context link relation have been detected. */ const MULTIPLE_CONTEXT_LINK_HEADERS = "multiple context link headers"; /** * There was a problem encountered loading a remote context. */ const LOADING_REMOTE_CONTEXT_FAILED = "loading remote context failed"; /** * No valid context document has been found for a referenced, * remote context. */ const INVALID_REMOTE_CONTEXT = "invalid remote context"; /** * A cycle in remote context inclusions has been detected. */ const RECURSIVE_CONTEXT_INCLUSION = "recursive context inclusion"; /** * An invalid base IRI has been detected, i.e., it is neither an * absolute IRI nor null. */ const INVALID_BASE_IRI = "invalid base IRI"; /** * An invalid vocabulary mapping has been detected, i.e., it is * neither an absolute IRI nor null. */ const INVALID_VOCAB_MAPPING = "invalid vocab mapping"; /** * The value of the default language is not a string or null and * thus invalid. */ const INVALID_DEFAULT_LANGUAGE = "invalid default language"; /** * A keyword redefinition has been detected. */ const KEYWORD_REDEFINITION = "keyword redefinition"; /** * An invalid term definition has been detected. */ const INVALID_TERM_DEFINITION = "invalid term definition"; /** * An invalid reverse property definition has been detected. */ const INVALID_REVERSE_PROPERTY = "invalid reverse property"; /** * IRI mapping A local context contains a term that has an invalid * or missing IRI mapping. */ const INVALID_IRI_MAPPING = "invalid IRI mapping"; /** * IRI mapping A cycle in IRI mappings has been detected. */ const CYCLIC_IRI_MAPPING = "cyclic IRI mapping"; /** * An invalid keyword alias definition has been encountered. */ const INVALID_KEYWORD_ALIAS = "invalid keyword alias"; /** * An @type member in a term definition was encountered whose value * could not be expanded to an absolute IRI. */ const INVALID_TYPE_MAPPING = "invalid type mapping"; /** * An @language member in a term definition was encountered whose * value was neither a string nor null and thus invalid. */ const INVALID_LANGUAGE_MAPPING = "invalid language mapping"; /** * Two properties which expand to the same keyword have been detected. * This might occur if a keyword and an alias thereof are used at the * same time. */ const COLLIDING_KEYWORDS = "colliding keywords"; /** * An @container member was encountered whose value was not one of * the following strings: @list, @set, or @index. */ const INVALID_CONTAINER_MAPPING = "invalid container mapping"; /** * An invalid value for an @type member has been detected, i.e., the * value was neither a string nor an array of strings. */ const INVALID_TYPE_VALUE = "invalid type value"; /** * A value object with disallowed members has been detected. */ const INVALID_VALUE_OBJECT = "invalid value object"; /** * An invalid value for the @value member of a value object has been * detected, i.e., it is neither a scalar nor null. */ const INVALID_VALUE_OBJECT_VALUE = "invalid value object value"; /** * A language-tagged string with an invalid language value was detected. */ const INVALID_LANGUAGE_TAGGED_STRING = "invalid language-tagged string"; /** * A number, true, or false with an associated language tag was detected. */ const INVALID_LANGUAGE_TAGGED_VALUE = "invalid language-tagged value"; /** * A typed value with an invalid type was detected. */ const INVALID_TYPED_VALUE = "invalid typed value"; /** * A set object or list object with disallowed members has been detected. */ const INVALID_SET_OR_LIST_OBJECT = "invalid set or list object"; /** * An invalid value in a language map has been detected. It has to be * a string or an array of strings. */ const INVALID_LANGUAGE_MAP_VALUE = "invalid language map value"; /** * The compacted document contains a list of lists as multiple lists * have been compacted to the same term. */ const COMPACTION_TO_LIST_OF_LISTS = "compaction to list of lists"; /** * An invalid reverse property map has been detected. No keywords apart * from @context are allowed in reverse property maps. */ const INVALID_REVERSE_PROPERTY_MAP = "invalid reverse property map"; /** * An invalid value for an @reverse member has been detected, i.e., the * value was not a JSON object. */ const INVALID_REVERSE_VALUE = "invalid @reverse value"; /** * An invalid value for a reverse property has been detected. The value * of an inverse property must be a node object. */ const INVALID_REVERSE_PROPERTY_VALUE = "invalid reverse property value"; /** * The JSON-LD snippet that triggered the error * * @var null|string */ private $snippet; /** * The document that triggered the error * * @var null|string */ private $document; /** * The raw error message (containing place-holders) * * @var string */ private $rawMessage; /** * Constructor. * * @param string $code The error code * @param null|string $message The error message * @param null|mixed $snippet The code snippet * @param null|string $document The document that triggered the error * @param null|\Exception $previous The previous exception */ public function __construct($code, $message = null, $snippet = null, $document = null, \Exception $previous = null) { $this->code = $code; $this->document = $document; $this->snippet = ($snippet) ? JsonLD::toString($snippet) : $snippet; $this->rawMessage = $message; $this->updateMessage(); parent::__construct($this->message, 0, $previous); } /** * Gets the snippet of code near the error. * * @return null|string The snippet of code */ public function getSnippet() { return $this->snippet; } /** * Gets the document that triggered the error * * @return null|string The document that triggered the error */ public function getParsedFile() { return $this->document; } /** * Updates the exception message by including the file name if available. */ private function updateMessage() { $this->message = $this->rawMessage; $dot = false; if ('.' === substr($this->message, -1)) { $this->message = substr($this->message, 0, -1); $dot = true; } if (null !== $this->document) { $this->message .= sprintf(' in %s', $this->document); } if ($this->snippet) { $this->message .= sprintf(' (near %s)', $this->snippet); } if ($dot) { $this->message .= '.'; } } } JsonLD-1.2.1/FileGetContentsLoader.php000066400000000000000000000172351431525543500175600ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\JsonLD\Exception\JsonLdException; use ML\IRI\IRI; /** * The FileGetContentsLoader loads remote documents by calling file_get_contents * * @author Markus Lanthaler */ class FileGetContentsLoader implements DocumentLoaderInterface { /** * {@inheritdoc} */ public function loadDocument($url) { // if input looks like a file, try to retrieve it $input = trim($url); if (false === (isset($input[0]) && ("{" === $input[0]) || ("[" === $input[0]))) { $remoteDocument = new RemoteDocument($url); $streamContextOptions = array( 'method' => 'GET', 'header' => "Accept: application/ld+json, application/json; q=0.9, */*; q=0.1\r\n" . "User-Agent: lanthaler JsonLD\r\n", 'timeout' => Processor::REMOTE_TIMEOUT ); $context = stream_context_create(array( 'http' => $streamContextOptions, 'https' => $streamContextOptions )); $httpHeadersOffset = 0; stream_context_set_params($context, array('notification' => function ($code, $severity, $msg, $msgCode, $bytesTx, $bytesMax) use ( &$remoteDocument, &$http_response_header, &$httpHeadersOffset ) { if ($code === STREAM_NOTIFY_MIME_TYPE_IS) { $remoteDocument->mediaType = $msg; } elseif ($code === STREAM_NOTIFY_REDIRECTED) { $remoteDocument->documentUrl = $msg; $remoteDocument->mediaType = null; $httpHeadersOffset = isset($http_response_header) ? count($http_response_header) : 0; } } )); if (false === ($input = @file_get_contents($url, false, $context))) { throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, sprintf('Unable to load the remote document "%s".', $url), $http_response_header ); } // Extract HTTP Link headers $linkHeaderValues = array(); if (is_array($http_response_header)) { for ($i = count($http_response_header) - 1; $i > $httpHeadersOffset; $i--) { if (0 === substr_compare($http_response_header[$i], 'Link:', 0, 5, true)) { $value = substr($http_response_header[$i], 5); $linkHeaderValues[] = $value; } } } $linkHeaderValues = $this->parseLinkHeaders($linkHeaderValues, new IRI($url)); $contextLinkHeaders = array_filter($linkHeaderValues, function ($link) { return (isset($link['rel']) && in_array('http://www.w3.org/ns/json-ld#context', explode(' ', $link['rel']))); }); if (count($contextLinkHeaders) === 1) { $remoteDocument->contextUrl = $contextLinkHeaders[0]['uri']; } elseif (count($contextLinkHeaders) > 1) { throw new JsonLdException( JsonLdException::MULTIPLE_CONTEXT_LINK_HEADERS, 'Found multiple contexts in HTTP Link headers', $http_response_header ); } // If we got a media type, we verify it if ($remoteDocument->mediaType) { // Drop any media type parameters such as profiles if (false !== ($pos = strpos($remoteDocument->mediaType, ';'))) { $remoteDocument->mediaType = substr($remoteDocument->mediaType, 0, $pos); } $remoteDocument->mediaType = trim($remoteDocument->mediaType); if ('application/ld+json' === $remoteDocument->mediaType) { $remoteDocument->contextUrl = null; } else { // If the Media type was not as expected, check to see if the desired content type // is being offered in a Link header (this is what schema.org now does). $altLinkHeaders = array_filter($linkHeaderValues, function ($link) { return (isset($link['rel']) && isset($link['type']) && ($link['rel'] === 'alternate') && ($link['type'] === 'application/ld+json')); }); // The spec states 'A response MUST NOT contain more than one HTTP Link Header // using the alternate link relation with type="application/ld+json"' if (count($altLinkHeaders) === 1) { return $this->loadDocument($altLinkHeaders[0]['uri']); } elseif (count($altLinkHeaders) > 1) { throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Received multiple alternate link headers' ); } if (('application/json' !== $remoteDocument->mediaType) && (0 !== substr_compare($remoteDocument->mediaType, '+json', -5))) { throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Invalid media type', $remoteDocument->mediaType ); } } } $remoteDocument->document = Processor::parse($input); return $remoteDocument; } return new RemoteDocument($url, Processor::parse($input)); } /** * Parse HTTP Link headers * * @param array $values An array of HTTP Link headers. * @param IRI $baseIri The document's URL (used to expand relative URLs to absolutes). * * @return array A structured representation of the Link header values. * * @internal Do not use this method directly, it's only temporarily accessible for testing. */ public function parseLinkHeaders(array $values, IRI $baseIri) { // Separate multiple links contained in a single header value for ($i = 0, $total = count($values); $i < $total; $i++) { if (strpos($values[$i], ',') !== false) { foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $values[$i]) as $v) { $values[] = trim($v); } unset($values[$i]); } } $contexts = $matches = array(); $trimWhitespaceCallback = function ($str) { return trim($str, "\"' \n\t"); }; // Split the header in key-value pairs $result = array(); foreach ($values as $val) { $part = array(); foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches); $pieces = array_map($trimWhitespaceCallback, $matches[0]); if (count($pieces) > 1) { $part[$pieces[0]] = $pieces[1]; } elseif (count($pieces) === 1) { $part['uri'] = (string) $baseIri->resolve(trim($pieces[0], '<> ')); } } if (!empty($part)) { $result[] = $part; } } return $result; } } JsonLD-1.2.1/Graph.php000066400000000000000000000151101431525543500144230ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\IRI\IRI; /** * A Graph represents a JSON-LD graph. * * @author Markus Lanthaler */ class Graph implements GraphInterface, JsonLdSerializable { /** * @var DocumentInterface The document this graph belongs to. */ private $document; /** * @var array An associative array holding all nodes in the graph */ protected $nodes = array(); /** * A term map containing terms/prefixes mapped to IRIs. This is similar * to a JSON-LD context but ignores all definitions except the IRI. * * @var array */ protected $termMap = array(); /** * @var int Blank node counter */ private $blankNodeCounter = 0; /** * Constructor * * @param null|DocumentInterface $document The document the graph belongs to. */ public function __construct(DocumentInterface $document = null) { $this->document = $document; } /** * {@inheritdoc} */ public function createNode($id = null, $preserveBnodeId = false) { if (!is_string($id) || (!$preserveBnodeId && ('_:' === substr($id, 0, 2)))) { $id = $this->createBlankNodeId(); } else { $id = (string) $this->resolveIri($id); if (isset($this->nodes[$id])) { return $this->nodes[$id]; } } return $this->nodes[$id] = new Node($this, $id); } /** * {@inheritdoc} */ public function removeNode(NodeInterface $node) { if ($node->getGraph() === $this) { $node->removeFromGraph(); } $id = $node->getId(); if (!$node->isBlankNode()) { $id = (string) $this->resolveIri($id); } unset($this->nodes[$id]); return $this; } /** * {@inheritdoc} */ public function getNodes() { return array_values($this->nodes); } /** * {@inheritdoc} */ public function getNode($id) { if (!((strlen($id) >= 2) && ('_:' === substr($id, 0, 2)))) { $id = (string) $this->resolveIri($id); } return isset($this->nodes[$id]) ? $this->nodes[$id] : null; } /** * {@inheritdoc} */ public function getNodesByType($type) { if (is_string($type)) { if (null === ($type = $this->getNode($type))) { return array(); } } return $type->getNodesWithThisType(); } /** * {@inheritdoc} */ public function containsNode($id) { $node = $id; if ($node instanceof Node) { $id = $node->getId(); } if ((null === $id) || !is_string($id)) { return false; } if ((strlen($id) >= 2) && ('_:' === substr($id, 0, 2))) { if (isset($this->nodes[$id]) && ($node === $this->nodes[$id])) { return true; } return false; } $id = (string) $this->resolveIri($id); return isset($this->nodes[$id]); } /** * {@inheritdoc} */ public function getDocument() { return $this->document; } /** * {@inheritdoc} */ public function removeFromDocument() { $doc = $this->document; $this->document = null; $doc->removeGraph($this); return $this; } /** * {@inheritdoc} */ public function merge(GraphInterface $graph) { $nodes = $graph->getNodes(); $bnodeMap = array(); foreach ($nodes as $node) { if ($node->isBlankNode()) { if (false === isset($bnodeMap[$node->getId()])) { $bnodeMap[$node->getId()] = $this->createNode(); } $n = $bnodeMap[$node->getId()]; } else { $n = $this->createNode($node->getId()); } foreach ($node->getProperties() as $property => $values) { if (false === is_array($values)) { $values = array($values); } foreach ($values as $val) { if ($val instanceof NodeInterface) { // If the value is another node, we just need to // create a reference to the corresponding node // in this graph. The properties will be merged // in the outer loop if ($val->isBlankNode()) { if (false === isset($bnodeMap[$val->getId()])) { $bnodeMap[$val->getId()] = $this->createNode(); } $val = $bnodeMap[$val->getId()]; } else { $val = $this->createNode($val->getId()); } } elseif (is_object($val)) { // Clone typed values and language-tagged strings $val = clone $val; } $n->addPropertyValue($property, $val); } } } return $this; } /** * {@inheritdoc} */ public function toJsonLd($useNativeTypes = true) { // Bring nodes into a deterministic order $nodes = $this->nodes; ksort($nodes); $nodes = array_values($nodes); $serializeNode = function ($node) use ($useNativeTypes) { return $node->toJsonLd($useNativeTypes); }; return array_map($serializeNode, $nodes); } /** * Create a new blank node identifier unique to the document. * * @return string The new blank node identifier. */ protected function createBlankNodeId() { return '_:b' . $this->blankNodeCounter++; } /** * Resolves an IRI against the document's IRI * * If the graph isn't attached to a document or the document's IRI is * not set, the IRI is returned as-is. * * @param string|IRI $iri The (relative) IRI to resolve * * @return IRI The resolved IRI. */ protected function resolveIri($iri) { if (null === $this->document) { $base = new IRI(); } else { $base = $this->document->getIri(true); } return $base->resolve($iri); } } JsonLD-1.2.1/GraphInterface.php000066400000000000000000000066311431525543500162540ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * JSON-LD graph interface * * @author Markus Lanthaler */ interface GraphInterface { /** * Creates a new node which is linked to this document * * If a blank node identifier or an invalid ID is passed, the ID will be * ignored and a new blank node identifier unique to the document is * created for the node. * * If there exists already a node with the passed ID in the document, * that node will be returned instead of creating a new one. * * @param null|string $id The ID of the node. * @param bool $preserveBnodeId If set to false, blank nodes are * relabeled to avoid collisions; * otherwise the blank node identifier * is preserved. * * @return Node The newly created node. */ public function createNode($id = null, $preserveBnodeId = false); /** * Removes a node from the document * * This will also eliminate all references to the node within the * document. * * @param NodeInterface $node The node to remove from the document. * * @return self */ public function removeNode(NodeInterface $node); /** * Get all nodes * * @return Node[] Returns an array containing all nodes defined in the * document. */ public function getNodes(); /** * Get a node by ID * * @param string $id The ID of the node to retrieve. * * @return Node|null Returns the node if found; null otherwise. */ public function getNode($id); /** * Get nodes by type * * @param string|Node $type The type * * @return Node[] Returns an array containing all nodes of the specified * type in the document. */ public function getNodesByType($type); /** * Check whether the document already contains a node with the * specified ID * * @param string|Node $id The node ID to check. Blank node identifiers * will always return false except a node instance * which is part of the document will be passed * instead of a string. * * @return bool Returns true if the document contains a node with the * specified ID; false otherwise. */ public function containsNode($id); /** * Get the document the node belongs to * * @return null|DocumentInterface Returns the document the node belongs * to or null if the node doesn't belong * to any document. */ public function getDocument(); /** * Removes the graph from the document * * @return self */ public function removeFromDocument(); /** * Merges the specified graph into the current graph * * @param GraphInterface $graph The graph that should be merged into the * current graph. * * @return self */ public function merge(GraphInterface $graph); } JsonLD-1.2.1/JsonLD.php000066400000000000000000000565231431525543500145300ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; use ML\JsonLD\Exception\JsonLdException; use ML\JsonLD\Exception\InvalidQuadException; use ML\IRI\IRI; /** * JsonLD * * JsonLD implements the algorithms defined by the * {@link http://www.w3.org/TR/json-ld-api/ JSON-LD 1.0 API and Processing Algorithms specification}. * Its interface is, apart from the usage of Promises, exactly the same as the one * defined by the specification. * * Furthermore, it implements an enhanced version of the * {@link http://json-ld.org/spec/latest/json-ld-framing/ JSON-LD Framing 1.0 draft} * and an object-oriented interface to access and manipulate JSON-LD documents. * * @api * * @author Markus Lanthaler */ class JsonLD { /** Identifier for the default graph */ const DEFAULT_GRAPH = '@default'; /** Identifier for the merged graph */ const MERGED_GRAPH = '@merged'; private static $documentLoader = null; /** * Load and parse a JSON-LD document * * The document can be supplied directly as string, by passing a file * path, or by passing a URL. * * Usage: * * $document = JsonLD::getDocument('document.jsonld'); * print_r($document->getGraphNames()); * * It is possible to configure the processing by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
documentFactory
*
The document factory.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to process. * @param null|array|JsonObject $options Options to configure the processing. * * @return Document The parsed JSON-LD document. * * @throws JsonLdException * * @api */ public static function getDocument($input, $options = null) { $options = self::mergeOptions($options); $input = self::expand($input, $options); $processor = new Processor($options); return $processor->getDocument($input); } /** * Expand a JSON-LD document * * The document can be supplied directly as string, by passing a file * path, or by passing a URL. * * Usage: * * $expanded = JsonLD::expand('document.jsonld'); * print_r($expanded); * * It is possible to configure the expansion process by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to expand. * @param null|array|JsonObject $options Options to configure the expansion * process. * * @return array The expanded JSON-LD document. * * @throws JsonLdException * * @api */ public static function expand($input, $options = null) { $options = self::mergeOptions($options); $processor = new Processor($options); $activectx = array('@base' => null); if (is_string($input)) { $remoteDocument = $options->documentLoader->loadDocument($input); $input = $remoteDocument->document; $activectx['@base'] = new IRI($remoteDocument->documentUrl); if (null !== $remoteDocument->contextUrl) { $processor->processContext($remoteDocument->contextUrl, $activectx); } } if ($options->base) { $activectx['@base'] = $options->base; } if (null !== $options->expandContext) { $processor->processContext($options->expandContext, $activectx); } $processor->expand($input, $activectx); // optimize away default graph (@graph as the only property at the top-level object) if (is_object($input) && property_exists($input, '@graph') && (1 === count(get_object_vars($input)))) { $input = $input->{'@graph'}; } if (false === is_array($input)) { $input = (null === $input) ? array() : array($input); } return $input; } /** * Compact a JSON-LD document according a supplied context * * Both the document and the context can be supplied directly as string, * by passing a file path, or by passing a URL. * * Usage: * * $compacted = JsonLD::compact('document.jsonld', 'context.jsonld'); * print_r($compacted); * * It is possible to configure the compaction process by setting the * options parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
optimize
*
If set to true, the processor is free to optimize the result to * produce an even compacter representation than the algorithm * described by the official JSON-LD specification.
* *
compactArrays
*
If set to true, arrays holding just one element are compacted * to scalars, otherwise the arrays are kept as arrays.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to * compact. * @param null|string|JsonObject|array $context The context. * @param null|array|JsonObject $options Options to configure the * compaction process. * * @return JsonObject The compacted JSON-LD document. * * @throws JsonLdException * * @api */ public static function compact($input, $context = null, $options = null) { $options = self::mergeOptions($options); $expanded = self::expand($input, $options); return self::doCompact($expanded, $context, $options); } /** * Compact a JSON-LD document according a supplied context * * In contrast to {@link compact()}, this method assumes that the input * has already been expanded. * * @param array $input The JSON-LD document to * compact. * @param null|string|JsonObject|array $context The context. * @param JsonObject $options Options to configure the * compaction process. * @param bool $alwaysGraph If set to true, the resulting * document will always explicitly * contain the default graph at * the top-level. * * @return JsonObject The compacted JSON-LD document. * * @throws JsonLdException */ private static function doCompact($input, $context, $options, $alwaysGraph = false) { if (is_string($context)) { $context = $options->documentLoader->loadDocument($context)->document; } if (is_object($context) && property_exists($context, '@context')) { $context = $context->{'@context'}; } if (is_object($context) && (0 === count(get_object_vars($context)))) { $context = null; } elseif (is_array($context) && (0 === count($context))) { $context = null; } $activectx = array('@base' => $options->base); $processor = new Processor($options); $processor->processContext($context, $activectx); $inversectx = $processor->createInverseContext($activectx); $processor->compact($input, $activectx, $inversectx); $compactedDocument = new JsonObject(); if (null !== $context) { $compactedDocument->{'@context'} = $context; } if ((false === is_array($input)) || (0 === count($input))) { if (false === $alwaysGraph) { $compactedDocument = (object) ((array) $compactedDocument + (array) $input); return $compactedDocument; } if (false === is_array($input)) { $input = array($input); } } $graphKeyword = (isset($inversectx['@graph']['term'])) ? $inversectx['@graph']['term'] : '@graph'; $compactedDocument->{$graphKeyword} = $input; return $compactedDocument; } /** * Flatten a JSON-LD document * * Both the document and the context can be supplied directly as string, * by passing a file path, or by passing a URL. * * Usage: * * $flattened = JsonLD::flatten('document.jsonld'); * print_r($flattened); * * It is possible to configure the flattening process by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
graph
*
The graph whose flattened representation should be returned. * The default graph is identified by {@link DEFAULT_GRAPH} and the * merged dataset graph by {@link MERGED_GRAPH}. If null is * passed, all graphs will be returned.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to flatten. * @param null|string|JsonObject|array $context The context to compact the * flattened document. If * null is passed, the * result will not be compacted. * @param null|array|JsonObject $options Options to configure the * flattening process. * * @return JsonObject The flattened JSON-LD document. * * @throws JsonLdException * * @api */ public static function flatten($input, $context = null, $options = null) { $options = self::mergeOptions($options); $input = self::expand($input, $options); $processor = new Processor($options); $flattened = $processor->flatten($input); if (null === $context) { return $flattened; } return self::doCompact($flattened, $context, $options, true); } /** * Convert a JSON-LD document to RDF quads * * The document can be supplied directly as string, by passing a file * path, or by passing a URL. * * Usage: * * $quads = JsonLD::toRdf('document.jsonld'); * print_r($quads); * * It is possible to configure the extraction process by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to expand. * @param null|array|JsonObject $options Options to configure the expansion * process. * * @return Quad[] The extracted quads. * * @throws JsonLdException * * @api */ public static function toRdf($input, $options = null) { $options = self::mergeOptions($options); $expanded = self::expand($input, $options); $processor = new Processor($options); return $processor->toRdf($expanded); } /** * Convert an array of RDF quads to a JSON-LD document * * Usage: * * $document = JsonLD::fromRdf($quads); * print(JsonLD::toString($document, true)); * * It is possible to configure the conversion process by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
useNativeTypes
*
If set to true, native types are used for xsd:integer, * xsd:double, and xsd:boolean; otherwise, * typed strings will be used instead.
* *
useRdfType
*
If set to true, rdf:type will be used instead of @type * *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param Quad[] $quads Array of quads. * @param null|array|JsonObject $options Options to configure the expansion * process. * * @return array The JSON-LD document in expanded form. * * @throws InvalidQuadException If an invalid quad was detected. * @throws JsonLdException If converting the quads to a JSON-LD document failed. * * @api */ public static function fromRdf(array $quads, $options = null) { $options = self::mergeOptions($options); $processor = new Processor($options); return $processor->fromRdf($quads); } /** * Frame a JSON-LD document according a supplied frame * * Both the document and the frame can be supplied directly as string, * by passing a file path, or by passing a URL. * * Usage: * * $result = JsonLD::frame('document.jsonld', 'frame.jsonldf'); * print_r($compacted); * * It is possible to configure the framing process by setting the options * parameter accordingly. Available options are: * *
*
base
*
The base IRI of the input document.
* *
expandContext
*
An optional context to use additionally to the context embedded * in input when expanding the input.
* *
optimize
*
If set to true, the processor is free to optimize the result to * produce an even compacter representation than the algorithm * described by the official JSON-LD specification.
* *
compactArrays
*
If set to true, arrays holding just one element are compacted * to scalars, otherwise the arrays are kept as arrays.
* *
documentLoader
*
The document loader.
*
* * The options parameter might be passed as associative array or as * object. * * @param string|JsonObject|array $input The JSON-LD document to compact. * @param string|JsonObject $frame The frame. * @param null|array|JsonObject $options Options to configure the framing * process. * * @return JsonObject The framed JSON-LD document. * * @throws JsonLdException * * @api */ public static function frame($input, $frame, $options = null) { $options = self::mergeOptions($options); $input = self::expand($input, $options); $frame = (is_string($frame)) ? $options->documentLoader->loadDocument($frame)->document : $frame; if (false === is_object($frame)) { throw new JsonLdException( JsonLdException::UNSPECIFIED, 'Invalid frame detected. It must be an object.', $frame ); } $processor = new Processor($options); // Store the frame's context as $frame gets modified $frameContext = new JsonObject(); if (property_exists($frame, '@context')) { $frameContext->{'@context'} = $frame->{'@context'}; } // Expand the frame $processor->expand($frame, array(), null, true); // and optimize away default graph (@graph as the only property at the top-level object) if (is_object($frame) && property_exists($frame, '@graph') && (1 === count(get_object_vars($frame)))) { $frame = $frame->{'@graph'}; } if (false === is_array($frame)) { $frame = array($frame); } // Frame the input document $result = $processor->frame($input, $frame); // Compact the result using the frame's active context return self::doCompact($result, $frameContext, $options, true); } /** * Convert the PHP structure returned by the various processing methods * to a string * * Usage: * * $compacted = JsonLD::compact('document.jsonld', 'context.jsonld'); * $prettyString = JsonLD::toString($compacted, true); * print($prettyString); * * @param mixed $value The value to convert. * @param bool $pretty Use whitespace in returned string to format it * (this just works in PHP >=5.4)? * * @return string */ public static function toString($value, $pretty = false) { $options = 0; if (PHP_VERSION_ID >= 50400) { $options |= JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; if ($pretty) { $options |= JSON_PRETTY_PRINT; } return json_encode($value, $options); } else { $result = json_encode($value); $result = str_replace('\\/', '/', $result); // unescape slahes // unescape unicode return preg_replace_callback( '/\\\\u([a-f0-9]{4})/', function ($match) { return iconv('UCS-4LE', 'UTF-8', pack('V', hexdec($match[1]))); }, $result ); } } /** * Merge the passed options with the options' default values. * * @param null|array|JsonObject $options The options. * * @return JsonObject The merged options. */ private static function mergeOptions($options) { $result = (object) array( 'base' => null, 'expandContext' => null, 'compactArrays' => true, 'optimize' => false, 'graph' => null, 'useNativeTypes' => false, 'useRdfType' => false, 'produceGeneralizedRdf' => false, 'documentFactory' => null, 'documentLoader' => new FileGetContentsLoader() ); if (is_array($options) || is_object($options)) { $options = (object) $options; if (isset($options->{'base'})) { if (is_string($options->{'base'})) { $result->base = new IRI($options->{'base'}); } elseif (($options->{'base'} instanceof IRI) && $options->{'base'}->isAbsolute()) { $result->base = clone $options->{'base'}; } else { throw new \InvalidArgumentException('The "base" option must be set to null or an absolute IRI.'); } } if (property_exists($options, 'compactArrays') && is_bool($options->compactArrays)) { $result->compactArrays = $options->compactArrays; } if (property_exists($options, 'optimize') && is_bool($options->optimize)) { $result->optimize = $options->optimize; } if (property_exists($options, 'graph') && is_string($options->graph)) { $result->graph = $options->graph; } if (property_exists($options, 'useNativeTypes') && is_bool($options->useNativeTypes)) { $result->useNativeTypes = $options->useNativeTypes; } if (property_exists($options, 'useRdfType') && is_bool($options->useRdfType)) { $result->useRdfType = $options->useRdfType; } if (property_exists($options, 'produceGeneralizedRdf') && is_bool($options->produceGeneralizedRdf)) { $result->produceGeneralizedRdf = $options->produceGeneralizedRdf; } if (property_exists($options, 'documentFactory') && ($options->documentFactory instanceof DocumentFactoryInterface)) { $result->documentFactory = $options->documentFactory; } if (property_exists($options, 'documentLoader') && ($options->documentLoader instanceof DocumentLoaderInterface)) { $result->documentLoader = $options->documentLoader; } elseif (null !== self::$documentLoader) { $result->documentLoader = self::$documentLoader; } if (property_exists($options, 'expandContext')) { if (is_string($options->expandContext)) { $result->expandContext = $result->documentLoader->loadDocument($options->expandContext)->document; } elseif (is_object($options->expandContext)) { $result->expandContext = $options->expandContext; } if (is_object($result->expandContext) && property_exists($result->expandContext, '@context')) { $result->expandContext = $result->expandContext->{'@context'}; } } } return $result; } /** * Set the default document loader. * * It can be overridden in individual operations by setting the * `documentLoader` option. * * @param DocumentLoaderInterface $documentLoader */ public static function setDefaultDocumentLoader(DocumentLoaderInterface $documentLoader) { self::$documentLoader = $documentLoader; } } JsonLD-1.2.1/JsonLdSerializable.php000066400000000000000000000022301431525543500171010ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * The JsonLdSerializable interface * * Objects implementing JsonLdSerializable can be serialized to JSON-LD. * * @author Markus Lanthaler */ interface JsonLdSerializable { /** * Convert to expanded and flattened JSON-LD * * The result can then be serialized to JSON-LD by {@see JsonLD::toString()}. * * @param boolean $useNativeTypes If set to true, native types are used * for xsd:integer, xsd:double, and * xsd:boolean, otherwise typed strings * will be used instead. * * @return mixed Returns data which can be serialized by * {@see JsonLD::toString()} (which is a value of any type * other than a resource) to expanded JSON-LD. * * @see JsonLD::toString() */ public function toJsonLd($useNativeTypes = true); } JsonLD-1.2.1/LICENSE000066400000000000000000000020511431525543500136560ustar00rootroot00000000000000Copyright (c) 2012-2016 Markus Lanthaler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. JsonLD-1.2.1/LanguageTaggedString.php000066400000000000000000000041331431525543500174130ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; /** * A LanguageTaggedString is a string which is tagged with a language. * * @author Markus Lanthaler */ final class LanguageTaggedString extends Value { /** * The language code associated with the string. Language codes are tags * according to {@link http://tools.ietf.org/html/bcp47 BCP47}. * * @var string */ private $language; /** * Constructor * * @param string $value The string's value. * @param string $language The string's language. */ public function __construct($value, $language) { $this->setValue($value); $this->setLanguage($language); } /** * Set the language * * @param string $language The language. * * @return self * * @throws \InvalidArgumentException If the language is not a string. No * further checks are currently done. */ public function setLanguage($language) { if (!is_string($language)) { throw new \InvalidArgumentException('language must be a string.'); } $this->language = $language; return $this; } /** * Get the language * * @return string The language. */ public function getLanguage() { return $this->language; } /** * {@inheritdoc} */ public function toJsonLd($useNativeTypes = true) { $result = new JsonObject(); $result->{'@value'} = $this->value; $result->{'@language'} = $this->language; return $result; } /** * {@inheritdoc} */ public function equals($other) { if (get_class($this) !== get_class($other)) { return false; } return ($this->value === $other->value) && ($this->language === $other->language); } } JsonLD-1.2.1/NQuads.php000066400000000000000000000130131431525543500145550ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\JsonLD\Exception\InvalidQuadException; use ML\IRI\IRI; /** * NQuads serializes quads to the NQuads format * * @author Markus Lanthaler */ class NQuads implements QuadSerializerInterface, QuadParserInterface { /** * {@inheritdoc} */ public function serialize(array $quads) { $result = ''; foreach ($quads as $quad) { $result .= ('_' === $quad->getSubject()->getScheme()) ? $quad->getSubject() : '<' . $quad->getSubject() . '>'; $result .= ' '; $result .= ('_' === $quad->getProperty()->getScheme()) ? $quad->getProperty() : '<' . $quad->getProperty() . '>'; $result .= ' '; if ($quad->getObject() instanceof IRI) { $result .= ('_' === $quad->getObject()->getScheme()) ? $quad->getObject() : '<' . $quad->getObject() . '>'; } else { $result .= '"' . str_replace( array("\n", '"'), array('\n', '\"'), $quad->getObject()->getValue()) . '"'; $result .= ($quad->getObject() instanceof TypedValue) ? (RdfConstants::XSD_STRING === $quad->getObject()->getType()) ? '' : '^^<' . $quad->getObject()->getType() . '>' : '@' . $quad->getObject()->getLanguage(); } $result .= ' '; if ($quad->getGraph()) { $result .= ('_' === $quad->getGraph()->getScheme()) ? $quad->getGraph() : '<' . $quad->getGraph() . '>'; $result .= ' '; } $result .= ".\n"; } return $result; } /** * {@inheritdoc} * * This method is heavily based on DigitalBazaar's implementation used * in their {@link https://github.com/digitalbazaar/php-json-ld php-json-ld}. * * @throws InvalidQuadException If an invalid quad that can't be parsed is * encountered. */ public function parse($input) { // define partial regexes $iri = '(?:<([^>]*)>)'; // blank node labels based on https://www.w3.org/TR/n-quads/#BNodes $bnode = '(_:(?:[A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9_\-.]*[A-Za-z0-9_\-]))'; $plain = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"'; $datatype = "\\^\\^$iri"; $language = '(?:@([a-z]+(?:-[a-z0-9]+)*))'; $literal = "(?:$plain(?:$datatype|$language)?)"; $ws = '[ \\t]'; $comment = "#.*"; $subject = "(?:$iri|$bnode)$ws+"; $property = "$iri$ws+"; $object = "(?:$iri|$bnode|$literal)"; $graph = "$ws+(?:$iri|$bnode)"; // full regexes $eoln = '/(?:(\r\n)|[\n\r])/'; $quadRegex = "/^$ws*$subject$property$object$graph?$ws*.$ws*$/"; $ignoreRegex = "/^$ws*(?:$comment)?$/"; // build RDF statements $statements = array(); // split N-Quad input into lines $lines = preg_split($eoln, $input); $line_number = 0; foreach ($lines as $line) { $line_number++; // skip empty lines if (preg_match($ignoreRegex, $line)) { continue; } // parse quad if (!preg_match($quadRegex, $line, $match)) { throw new InvalidQuadException( sprintf( 'Error while parsing N-Quads. Invalid quad in line %d: %s', $line_number, $line ), $line ); } // get subject if ($match[1] !== '') { $subject = new IRI($match[1]); } else { $subject = new IRI($match[2]); } // get property $property = new IRI($match[3]); // get object if ($match[4] !== '') { $object = new IRI($match[4]); // IRI } elseif ($match[5] !== '') { $object = new IRI($match[5]); // bnode } else { $unescaped = str_replace( array('\"', '\t', '\n', '\r', '\\\\'), array('"', "\t", "\n", "\r", '\\'), $match[6] ); if (isset($match[7]) && $match[7] !== '') { $object = new TypedValue($unescaped, $match[7]); } elseif (isset($match[8]) && $match[8] !== '') { $object = new LanguageTaggedString($unescaped, $match[8]); } else { $object = new TypedValue($unescaped, RdfConstants::XSD_STRING); } } // get graph $graph = null; if (isset($match[9]) && $match[9] !== '') { $graph = new IRI($match[9]); } elseif (isset($match[10]) && $match[10] !== '') { $graph = new IRI($match[10]); } $quad = new Quad($subject, $property, $object, $graph); // TODO Make sure that quads are unique?? $statements[] = $quad; } return $statements; } } JsonLD-1.2.1/Node.php000066400000000000000000000317431431525543500142610ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; /** * A Node represents a node in a JSON-LD graph. * * @author Markus Lanthaler */ class Node implements NodeInterface, JsonLdSerializable { /** The @type constant. */ const TYPE = '@type'; /** * @var GraphInterface The graph the node belongs to. */ private $graph; /** * @var string The ID of the node */ private $id; /** * @var array An associative array holding all properties of the node except it's ID */ private $properties = array(); /** * An associative array holding all reverse properties of this node, i.e., * a pointers to all nodes that link to this node. * * @var array */ private $revProperties = array(); /** * Constructor * * @param GraphInterface $graph The graph the node belongs to. * @param null|string $id The ID of the node. */ public function __construct(GraphInterface $graph, $id = null) { $this->graph = $graph; $this->id = $id; } /** * {@inheritdoc} */ public function getId() { return $this->id; } /** * {@inheritdoc} */ public function setType($type) { if ((null !== $type) && !($type instanceof NodeInterface)) { if (is_array($type)) { foreach ($type as $val) { if ((null !== $val) && !($val instanceof NodeInterface)) { throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes'); } } } else { throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes'); } } $this->setProperty(self::TYPE, $type); return $this; } /** * {@inheritdoc} */ public function addType(NodeInterface $type) { $this->addPropertyValue(self::TYPE, $type); return $this; } /** * {@inheritdoc} */ public function removeType(NodeInterface $type) { $this->removePropertyValue(self::TYPE, $type); return $this; } /** * {@inheritdoc} */ public function getType() { return $this->getProperty(self::TYPE); } /** * {@inheritdoc} */ public function getNodesWithThisType() { if (null === ($nodes = $this->getReverseProperty(self::TYPE))) { return array(); } return (is_array($nodes)) ? $nodes : array($nodes); } /** * {@inheritdoc} */ public function getGraph() { return $this->graph; } /** * {@inheritdoc} */ public function removeFromGraph() { // Remove other node's properties and reverse properties pointing to // this node foreach ($this->revProperties as $property => $nodes) { foreach ($nodes as $node) { $node->removePropertyValue($property, $this); } } foreach ($this->properties as $property => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if ($value instanceof NodeInterface) { $this->removePropertyValue($property, $value); } } } $g = $this->graph; $this->graph = null; $g->removeNode($this); return $this; } /** * {@inheritdoc} */ public function isBlankNode() { return ((null === $this->id) || ('_:' === substr($this->id, 0, 2))); } /** * {@inheritdoc} */ public function setProperty($property, $value) { if (null === $value) { $this->removeProperty($property); } else { $this->doMergeIntoProperty((string) $property, array(), $value); } return $this; } /** * {@inheritdoc} */ public function addPropertyValue($property, $value) { $existing = (isset($this->properties[(string) $property])) ? $this->properties[(string) $property] : array(); if (!is_array($existing)) { $existing = array($existing); } $this->doMergeIntoProperty((string) $property, $existing, $value); return $this; } /** * Merge a value into a set of existing values. * * @param string $property The name of the property. * @param array $existingValues The existing values. * @param mixed $value The value to merge into the existing * values. This MUST NOT be an array. * * @throws \InvalidArgumentException If value is an array or an object * which is neither a language-tagged * string nor a typed value or a node. */ private function doMergeIntoProperty($property, $existingValues, $value) { // TODO: Handle lists! if (null === $value) { return; } if (!$this->isValidPropertyValue($value)) { throw new \InvalidArgumentException( 'value must be a scalar, a node, a language-tagged string, or a typed value' ); } $normalizedValue = $this->normalizePropertyValue($value); foreach ($existingValues as $existing) { if ($this->equalValues($existing, $normalizedValue)) { return; } } $existingValues[] = $normalizedValue; if (1 === count($existingValues)) { $existingValues = $existingValues[0]; } $this->properties[$property] = $existingValues; if ($normalizedValue instanceof NodeInterface) { $value->addReverseProperty($property, $this); } } /** * {@inheritdoc} */ public function removeProperty($property) { if (!isset($this->properties[(string) $property])) { return $this; } $values = is_array($this->properties[(string) $property]) ? $this->properties[(string) $property] : array($this->properties[(string) $property]); foreach ($values as $value) { if ($value instanceof NodeInterface) { $value->removeReverseProperty((string) $property, $this); } } unset($this->properties[(string) $property]); return $this; } /** * {@inheritdoc} */ public function removePropertyValue($property, $value) { if (!$this->isValidPropertyValue($value) || !isset($this->properties[(string) $property])) { return $this; } $normalizedValue = $this->normalizePropertyValue($value); $values =& $this->properties[(string) $property]; if (!is_array($this->properties[(string) $property])) { $values = array($values); } for ($i = 0, $length = count($values); $i < $length; $i++) { if ($this->equalValues($values[$i], $normalizedValue)) { if ($normalizedValue instanceof NodeInterface) { $normalizedValue->removeReverseProperty((string) $property, $this); } unset($values[$i]); break; } } if (0 === count($values)) { unset($this->properties[(string) $property]); return $this; } $this->properties[(string) $property] = array_values($values); // re-index the array if (1 === count($this->properties[(string) $property])) { $this->properties[(string) $property] = $this->properties[(string) $property][0]; } } /** * {@inheritdoc} */ public function getProperties() { return $this->properties; } /** * {@inheritdoc} */ public function getProperty($property) { return (isset($this->properties[(string) $property])) ? $this->properties[(string) $property] : null; } /** * {@inheritdoc} */ public function getReverseProperties() { $result = array(); foreach ($this->revProperties as $key => $nodes) { $result[$key] = array_values($nodes); } return $result; } /** * {@inheritdoc} */ public function getReverseProperty($property) { if (!isset($this->revProperties[(string) $property])) { return null; } $result = array_values($this->revProperties[(string) $property]); return (1 === count($result)) ? $result[0] : $result; } /** * {@inheritdoc} */ public function equals(NodeInterface $other) { return $this === $other; } /** * {@inheritdoc} */ public function toJsonLd($useNativeTypes = true) { $node = new \stdClass(); // Only label blank nodes if other nodes point to it if ((false === $this->isBlankNode()) || (count($this->getReverseProperties()) > 0)) { $node->{'@id'} = $this->getId(); } $properties = $this->getProperties(); foreach ($properties as $prop => $values) { if (false === is_array($values)) { $values = array($values); } if (self::TYPE === $prop) { $node->{'@type'} = array(); foreach ($values as $val) { $node->{'@type'}[] = $val->getId(); } continue; } $node->{$prop} = array(); foreach ($values as $value) { if ($value instanceof NodeInterface) { $ref = new \stdClass(); $ref->{'@id'} = $value->getId(); $node->{$prop}[] = $ref; } elseif (is_object($value)) { // language-tagged string or typed value $node->{$prop}[] = $value->toJsonLd($useNativeTypes); } else { $val = new JsonObject(); $val->{'@value'} = $value; $node->{$prop}[] = $val; } } } return $node; } /** * Add a reverse property. * * @param string $property The name of the property. * @param NodeInterface $node The node which has a property pointing * to this node instance. */ protected function addReverseProperty($property, NodeInterface $node) { $this->revProperties[$property][$node->getId()] = $node; } /** * Remove a reverse property. * * @param string $property The name of the property. * @param NodeInterface $node The node which has a property pointing * to this node instance. */ protected function removeReverseProperty($property, NodeInterface $node) { unset($this->revProperties[$property][$node->getId()]); if (0 === count($this->revProperties[$property])) { unset($this->revProperties[$property]); } } /** * Checks whether a value is a valid property value. * * @param mixed $value The value to check. * * @return bool Returns true if the value is a valid property value; * false otherwise. */ protected function isValidPropertyValue($value) { return (is_scalar($value) || (is_object($value) && ((($value instanceof NodeInterface) && ($value->getGraph() === $this->graph)) || ($value instanceof Value)))); } /** * Normalizes a property value by converting scalars to Value objects. * * @param mixed $value The value to normalize. * * @return NodeInterface|Value The normalized value. */ protected function normalizePropertyValue($value) { if (false === is_scalar($value)) { return $value; } return Value::fromJsonLd((object) array('@value' => $value)); } /** * Checks whether the two specified values are the same. * * Scalars and nodes are checked for identity, value objects for * equality. * * @param mixed $value1 Value 1. * @param mixed $value2 Value 2. * * @return bool Returns true if the two values are equals; otherwise false. */ protected function equalValues($value1, $value2) { if (gettype($value1) !== gettype($value2)) { return false; } if (is_object($value1) && ($value1 instanceof Value)) { return $value1->equals($value2); } return ($value1 === $value2); } } JsonLD-1.2.1/NodeInterface.php000066400000000000000000000151431431525543500160760ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * A generic interface for nodes in a JSON-LD graph. * * @author Markus Lanthaler */ interface NodeInterface { /** * Get ID * * @return string|null The ID of the node or null. */ public function getId(); /** * Set the node type * * @param null|NodeInterface|array[NodeInterface] The type(s) of this node. * * @return self * * @throws \InvalidArgumentException If type is not null, a Node or an * array of Nodes. */ public function setType($type); /** * Add a type to this node * * @param NodeInterface The type to add. * * @return self */ public function addType(NodeInterface $type); /** * Remove a type from this node * * @param NodeInterface The type to remove. * * @return self */ public function removeType(NodeInterface $type); /** * Get node type * * @return null|NodeInterface|NodeInterface[] Returns the type(s) of this node. */ public function getType(); /** * Get the nodes which have this node as their type * * This will return all nodes that link to this Node instance via the * @type (rdf:type) property. * * @return NodeInterface[] Returns the node(s) having this node as their * type. */ public function getNodesWithThisType(); /** * Get the graph the node belongs to * * @return null|GraphInterface Returns the graph the node belongs to or * null if the node doesn't belong to any graph. */ public function getGraph(); /** * Removes the node from the graph * * This will also remove all references to and from other nodes in this * node's graph. * * @return self */ public function removeFromGraph(); /** * Is this node a blank node * * A blank node is a node whose identifier has just local meaning. It has * therefore a node identifier with the prefix _: or no * identifier at all. * * @return bool Returns true if the node is a blank node, otherwise false. */ public function isBlankNode(); /** * Set a property of the node * * If the value is or contains a reference to a node which is not part * of the graph, the referenced node will added to the graph as well. * If the referenced node is already part of another graph a copy of the * node will be created and added to the graph. * * @param string $property The name of the property. * @param mixed $value The value of the property. This MUST NOT be * an array. Use null to remove the property. * * @return self * * @throws \InvalidArgumentException If value is an array or an object * which is neither a language-tagged * string nor a typed value or a node. */ public function setProperty($property, $value); /** * Adds a value to a property of the node * * If the value already exists, it won't be added again, i.e., there * won't be any duplicate property values. * * If the value is or contains a reference to a node which is not part * of the graph, the referenced node will added to the graph as well. * If the referenced node is already part of another graph a copy of the * node will be created and added to the graph. * * @param string $property The name of the property. * @param mixed $value The value of the property. This MUST NOT be * an array. * * @return self * * @throws \InvalidArgumentException If value is an array or an object * which is neither a language-tagged * string nor a typed value or a node. */ public function addPropertyValue($property, $value); /** * Removes a property and all it's values * * @param string $property The name of the property to remove. * * @return self */ public function removeProperty($property); /** * Removes a property value * * @param string $property The name of the property. * @param mixed $value The value of the property. This MUST NOT be * an array. * * @return self */ public function removePropertyValue($property, $value); /** * Get the properties of this node * * @return array Returns an associative array containing all properties * of this node. The key is the property name whereas the * value is the property's value. */ public function getProperties(); /** * Get the value of a property * * @param string $property The name of the property. * * @return mixed Returns the value of the property or null if the * property doesn't exist. */ public function getProperty($property); /** * Get the reverse properties of this node * * @return array Returns an associative array containing all reverse * properties of this node. The key is the property name * whereas the value is an array of nodes linking to this * instance via that property. */ public function getReverseProperties(); /** * Get the nodes of a reverse property * * This will return all nodes that link to this Node instance via the * specified property. * * @param string $property The name of the reverse property. * * @return null|NodeInterface|NodeInterface[] Returns the node(s) pointing * to this instance via the specified * property or null if no such node exists. */ public function getReverseProperty($property); /** * Compares this node object to the specified value. * * @param mixed $other The value this instance should be compared to. * * @return bool Returns true if the passed value is the same as this * instance; false otherwise. */ public function equals(NodeInterface $other); } JsonLD-1.2.1/Processor.php000066400000000000000000003361671431525543500153630ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; use ML\JsonLD\Exception\JsonLdException; use ML\JsonLD\Exception\InvalidQuadException; use ML\IRI\IRI; /** * Processor processes JSON-LD documents as specified by the JSON-LD * specification. * * @author Markus Lanthaler */ class Processor { /** Timeout for retrieving remote documents in seconds */ const REMOTE_TIMEOUT = 10; /** Maximum number of recursion that are allowed to resolve an IRI */ const CONTEXT_MAX_IRI_RECURSIONS = 10; /** * @var array A list of all defined keywords */ private static $keywords = array('@context', '@id', '@value', '@language', '@type', '@container', '@list', '@set', '@graph', '@reverse', '@base', '@vocab', '@index', '@null'); // TODO Introduce @null supported just for framing /** * @var array Framing options keywords */ private static $framingKeywords = array('@explicit', '@default', '@embed', //'@omitDefault', // TODO Is this really needed? '@embedChildren'); // TODO How should this be called? // TODO Add @preserve, @null?? Update spec keyword list /** * @var IRI The base IRI */ private $baseIri = null; /** * Compact arrays with just one element to a scalar * * If set to true, arrays holding just one element are compacted to * scalars, otherwise the arrays are kept as arrays. * * @var bool */ private $compactArrays; /** * Optimize compacted output * * If set to true, the processor is free to optimize the result to produce * an even compacter representation than the algorithm described by the * official JSON-LD specification. * * @var bool */ private $optimize; /** * Use native types when converting from RDF * * If set to true, the processor will try to convert datatyped literals * to native types instead of using the expanded object form when * converting from RDF. xsd:boolean values will be converted to booleans * whereas xsd:integer and xsd:double values will be converted to numbers. * * @var bool */ private $useNativeTypes; /** * Use rdf:type instead of \@type when converting from RDF * * If set to true, the JSON-LD processor will use the expanded rdf:type * IRI as the property instead of \@type when converting from RDF. * * @var bool */ private $useRdfType; /** * Produce generalized RDF * * Unless set to true, triples/quads with a blank node predicate are * dropped when converting to RDF. * * @var bool */ private $generalizedRdf; /** * @var array Blank node map */ private $blankNodeMap = array(); /** * @var integer Blank node counter */ private $blankNodeCounter = 0; /** * @var DocumentFactoryInterface The factory to create new documents */ private $documentFactory = null; /** * @var DocumentLoaderInterface The document loader */ private $documentLoader = null; /** * Constructor * * The options parameter must be passed and all off the following properties * have to be set: * *
*
base
*
The base IRI.
* *
compactArrays
*
If set to true, arrays holding just one element are compacted * to scalars, otherwise the arrays are kept as arrays.
* *
optimize
*
If set to true, the processor is free to optimize the result to * produce an even compacter representation than the algorithm * described by the official JSON-LD specification.
* *
useNativeTypes
*
If set to true, the processor will try to convert datatyped * literals to native types instead of using the expanded object form * when converting from RDF. xsd:boolean values will be * converted to booleans whereas xsd:integer and * xsd:double values will be converted to numbers.
* *
useRdfType
*
If set to true, the JSON-LD processor will use the expanded * rdf:type IRI as the property instead of @type * when converting from RDF.
*
* * @param JsonObject $options Options to configure the various algorithms. */ public function __construct($options) { $this->baseIri = new IRI($options->base); $this->compactArrays = (bool) $options->compactArrays; $this->optimize = (bool) $options->optimize; $this->useNativeTypes = (bool) $options->useNativeTypes; $this->useRdfType = (bool) $options->useRdfType; $this->generalizedRdf = (bool) $options->produceGeneralizedRdf; $this->documentFactory = $options->documentFactory; $this->documentLoader = $options->documentLoader; } /** * Parses a JSON-LD document to a PHP value * * @param string $document A JSON-LD document. * * @return mixed A PHP value. * * @throws JsonLdException If the JSON-LD document is not valid. */ public static function parse($document) { if (function_exists('mb_detect_encoding') && (false === mb_detect_encoding($document, 'UTF-8', true))) { throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'The JSON-LD document does not appear to be valid UTF-8.' ); } $data = json_decode($document, false, 512); switch (json_last_error()) { case JSON_ERROR_NONE: break; // no error case JSON_ERROR_DEPTH: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'The maximum stack depth has been exceeded.' ); case JSON_ERROR_STATE_MISMATCH: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Invalid or malformed JSON.' ); case JSON_ERROR_CTRL_CHAR: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Control character error (possibly incorrectly encoded).' ); case JSON_ERROR_SYNTAX: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Syntax error, malformed JSON.' ); case JSON_ERROR_UTF8: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Malformed UTF-8 characters (possibly incorrectly encoded).' ); default: throw new JsonLdException( JsonLdException::LOADING_DOCUMENT_FAILED, 'Unknown error while parsing JSON.' ); } return (empty($data)) ? null : $data; } /** * Parses a JSON-LD document and returns it as a Document * * @param array|JsonObject $input The JSON-LD document to process. * * @return Document The parsed JSON-LD document. * * @throws JsonLdException If the JSON-LD input document is invalid. */ public function getDocument($input) { $nodeMap = new JsonObject(); $nodeMap->{'-' . JsonLD::DEFAULT_GRAPH} = new JsonObject(); $this->generateNodeMap($nodeMap, $input); // We need to keep track of blank nodes as they are renamed when // inserted into the Document $nodes = array(); if (null === $this->documentFactory) { $this->documentFactory = new DefaultDocumentFactory(); } $document = $this->documentFactory->createDocument($this->baseIri); foreach ($nodeMap as $graphName => &$nodes) { $graphName = substr($graphName, 1); if (JsonLD::DEFAULT_GRAPH === $graphName) { $graph = $document->getGraph(); } else { $graph = $document->createGraph($graphName); } foreach ($nodes as $id => &$item) { $node = $graph->createNode($item->{'@id'}, true); unset($item->{'@id'}); // Process node type as it needs to be handled differently than // other properties // TODO Could this be avoided by enforcing rdf:type instead of @type? if (property_exists($item, '@type')) { foreach ($item->{'@type'} as $type) { $node->addType($graph->createNode($type), true); } unset($item->{'@type'}); } foreach ($item as $property => $values) { foreach ($values as $value) { if (property_exists($value, '@value')) { $node->addPropertyValue($property, Value::fromJsonLd($value)); } elseif (property_exists($value, '@id')) { $node->addPropertyValue( $property, $graph->createNode($value->{'@id'}, true) ); } else { // TODO Handle lists throw new \Exception('Lists are not supported by getDocument() yet'); } } } } } unset($nodeMap); return $document; } /** * Expands a JSON-LD document * * @param mixed $element A JSON-LD element to be expanded. * @param array $activectx The active context. * @param null|string $activeprty The active property. * @param boolean $frame True if a frame is being expanded, otherwise false. * * @return mixed The expanded document. * * @throws JsonLdException */ public function expand(&$element, $activectx = array(), $activeprty = null, $frame = false) { if (is_scalar($element)) { if ((null === $activeprty) || ('@graph' === $activeprty)) { $element = null; } else { $element = $this->expandValue($element, $activectx, $activeprty); } return; } if (null === $element) { return; } if (is_array($element)) { $result = array(); foreach ($element as &$item) { $this->expand($item, $activectx, $activeprty, $frame); // Check for lists of lists if (('@list' === $this->getPropertyDefinition($activectx, $activeprty, '@container')) || ('@list' === $activeprty)) { if (is_array($item) || (is_object($item) && property_exists($item, '@list'))) { throw new JsonLdException( JsonLdException::LIST_OF_LISTS, "List of lists detected in property \"$activeprty\".", $element ); } } if (is_array($item)) { $result = array_merge($result, $item); } elseif (null !== $item) { $result[] = $item; } } $element = $result; return; } // Otherwise it's an object. Process its local context if available if (property_exists($element, '@context')) { $this->processContext($element->{'@context'}, $activectx); unset($element->{'@context'}); } $properties = get_object_vars($element); ksort($properties); $element = new JsonObject(); foreach ($properties as $property => $value) { $expProperty = $this->expandIri($property, $activectx, false, true); // Make sure to keep framing keywords if a frame is being expanded if ($frame && in_array($expProperty, self::$framingKeywords)) { // and that the default value is expanded if ('@default' === $expProperty) { $this->expand($value, $activectx, $activeprty, $frame); } self::setProperty($element, $expProperty, $value, JsonLdException::COLLIDING_KEYWORDS); continue; } if (in_array($expProperty, self::$keywords)) { if ('@reverse' === $activeprty) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_PROPERTY_MAP, 'No keywords or keyword aliases are allowed in @reverse-maps, found ' . $expProperty ); } $this->expandKeywordValue($element, $activeprty, $expProperty, $value, $activectx, $frame); continue; } elseif (false === strpos($expProperty, ':')) { // the expanded property is neither a keyword nor an IRI continue; } $propertyContainer = $this->getPropertyDefinition($activectx, $property, '@container'); if (is_object($value) && in_array($propertyContainer, array('@language', '@index'))) { $result = array(); $value = (array) $value; // makes it easier to order the key-value pairs ksort($value); if ('@language' === $propertyContainer) { foreach ($value as $key => $val) { // TODO Make sure key is a valid language tag if (false === is_array($val)) { $val = array($val); } foreach ($val as $item) { if (false === is_string($item)) { throw new JsonLdException( JsonLdException::INVALID_LANGUAGE_MAP_VALUE, "Detected invalid value in $property->$key: it must be a string as it " . "is part of a language map.", $item ); } $result[] = (object) array( '@value' => $item, '@language' => strtolower($key) ); } } } else { // @container: @index foreach ($value as $key => $val) { if (false === is_array($val)) { $val = array($val); } $this->expand($val, $activectx, $property, $frame); foreach ($val as $item) { if (false === property_exists($item, '@index')) { $item->{'@index'} = $key; } $result[] = $item; } } } $value = $result; } else { $this->expand($value, $activectx, $property, $frame); } // Remove properties with null values if (null === $value) { continue; } // If property has an @list container and value is not yet an // expanded @list-object, transform it to one if (('@list' === $propertyContainer) && ((false === is_object($value) || (false === property_exists($value, '@list'))))) { if (false === is_array($value)) { $value = array($value); } $obj = new JsonObject(); $obj->{'@list'} = $value; $value = $obj; } $target = $element; if ($this->getPropertyDefinition($activectx, $property, '@reverse')) { if (false === property_exists($target, '@reverse')) { $target->{'@reverse'} = new JsonObject(); } $target = $target->{'@reverse'}; if (false === is_array($value)) { $value = array($value); } foreach ($value as $val) { if (property_exists($val, '@value') || property_exists($val, '@list')) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_PROPERTY_VALUE, 'Detected invalid value in @reverse-map (only nodes are allowed', $val ); } } } self::mergeIntoProperty($target, $expProperty, $value, true); } // All properties have been processed. Make sure the result is valid // and optimize it where possible $numProps = count(get_object_vars($element)); // Remove free-floating nodes if ((false === $frame) && ((null === $activeprty) || ('@graph' === $activeprty)) && (((0 === $numProps) || property_exists($element, '@value') || property_exists($element, '@list') || ((1 === $numProps) && property_exists($element, '@id'))))) { $element = null; return; } // Indexes are allowed everywhere if (property_exists($element, '@index')) { $numProps--; } if (property_exists($element, '@value')) { $numProps--; // @value if (property_exists($element, '@language')) { if (false === $frame) { if (false === is_string($element->{'@language'})) { throw new JsonLdException( JsonLdException::INVALID_LANGUAGE_TAGGED_STRING, 'Invalid value for @language detected (must be a string).', $element ); } if (false === is_string($element->{'@value'})) { throw new JsonLdException( JsonLdException::INVALID_LANGUAGE_TAGGED_VALUE, 'Only strings can be language tagged.', $element ); } } $numProps--; } elseif (property_exists($element, '@type')) { if ((false === $frame) && ((false === is_string($element->{'@type'})) || (false === strpos($element->{'@type'}, ':')) || ('_:' === substr($element->{'@type'}, 0, 2)))) { throw new JsonLdException( JsonLdException::INVALID_TYPED_VALUE, 'Invalid value for @type detected (must be an IRI).', $element ); } $numProps--; } if ($numProps > 0) { throw new JsonLdException( JsonLdException::INVALID_VALUE_OBJECT, 'Detected an invalid @value object.', $element ); } elseif (null === $element->{'@value'}) { // object has just an @value property that is null, can be replaced with that value $element = $element->{'@value'}; } return; } // Not an @value object, make sure @type is an array if (property_exists($element, '@type') && (false === is_array($element->{'@type'}))) { $element->{'@type'} = array($element->{'@type'}); } if (($numProps > 1) && ((property_exists($element, '@list') || property_exists($element, '@set')))) { throw new JsonLdException( JsonLdException::INVALID_SET_OR_LIST_OBJECT, 'An object with a @list or @set property can\'t contain other properties.', $element ); } elseif (property_exists($element, '@set')) { // @set objects can be optimized away as they are just syntactic sugar $element = $element->{'@set'}; } elseif (($numProps === 1) && (false === $frame) && property_exists($element, '@language')) { // if there's just @language and nothing else and we are not expanding a frame, drop whole object $element = null; } } /** * Expands the value of a keyword * * @param JsonObject $element The object this property-value pair is part of. * @param string $activeprty The active property. * @param string $keyword The keyword whose value is being expanded. * @param mixed $value The value to expand. * @param array $activectx The active context. * @param boolean $frame True if a frame is being expanded, otherwise false. * * @throws JsonLdException */ private function expandKeywordValue(&$element, $activeprty, $keyword, $value, $activectx, $frame) { // Ignore all null values except for @value as in that case it is // needed to determine what @type means if ((null === $value) && ('@value' !== $keyword)) { return; } if ('@id' === $keyword) { if (false === is_string($value)) { throw new JsonLdException( JsonLdException::INVALID_ID_VALUE, 'Invalid value for @id detected (must be a string).', $element ); } $value = $this->expandIri($value, $activectx, true); self::setProperty($element, $keyword, $value, JsonLdException::COLLIDING_KEYWORDS); return; } if ('@type' === $keyword) { if (is_string($value)) { $value = $this->expandIri($value, $activectx, true, true); self::setProperty($element, $keyword, $value, JsonLdException::COLLIDING_KEYWORDS); return; } if (false === is_array($value)) { $value = array($value); } $result = array(); foreach ($value as $item) { if (is_string($item)) { $result[] = $this->expandIri($item, $activectx, true, true); } else { if (false === $frame) { throw new JsonLdException( JsonLdException::INVALID_TYPE_VALUE, "Invalid value for $keyword detected.", $value ); } self::mergeIntoProperty($element, $keyword, $item); } } // Don't keep empty arrays if (count($result) >= 1) { self::mergeIntoProperty($element, $keyword, $result, true); } } if (('@value' === $keyword)) { if (false === $frame) { if ((null !== $value) && (false === is_scalar($value))) { // we need to preserve @value: null to distinguish values form nodes throw new JsonLdException( JsonLdException::INVALID_VALUE_OBJECT_VALUE, "Invalid value for @value detected (must be a scalar).", $value ); } } elseif (false === is_array($value)) { $value = array($value); } self::setProperty($element, $keyword, $value, JsonLdException::COLLIDING_KEYWORDS); return; } if (('@language' === $keyword) || ('@index' === $keyword)) { if (false === $frame) { if (false === is_string($value)) { throw ('@language' === $keyword) ? new JsonLdException( JsonLdException::INVALID_LANGUAGE_TAGGED_STRING, '@language must be a string', $value ) : new JsonLdException( JsonLdException::INVALID_INDEX_VALUE, '@index must be a string', $value ); } } elseif (false === is_array($value)) { $value = array($value); } self::setProperty($element, $keyword, $value, JsonLdException::COLLIDING_KEYWORDS); return; } // TODO Optimize the following code, there's a lot of repetition, only the $activeprty param is changing if ('@list' === $keyword) { if ((null === $activeprty) || ('@graph' === $activeprty)) { return; } $this->expand($value, $activectx, $activeprty, $frame); if (false === is_array($value)) { $value = array($value); } foreach ($value as $val) { if (is_object($val) && property_exists($val, '@list')) { throw new JsonLdException(JsonLdException::LIST_OF_LISTS, 'List of lists detected.', $element); } } self::mergeIntoProperty($element, $keyword, $value, true); return; } if ('@set' === $keyword) { $this->expand($value, $activectx, $activeprty, $frame); self::mergeIntoProperty($element, $keyword, $value, true); return; } if ('@reverse' === $keyword) { if (false === is_object($value)) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_VALUE, 'Detected invalid value for @reverse (must be an object).', $value ); } $this->expand($value, $activectx, $keyword, $frame); // Do not create @reverse-containers inside @reverse containers if (property_exists($value, $keyword)) { foreach (get_object_vars($value->{$keyword}) as $prop => $val) { self::mergeIntoProperty($element, $prop, $val, true); } unset($value->{$keyword}); } $value = get_object_vars($value); if ((count($value) > 0) && (false === property_exists($element, $keyword))) { $element->{$keyword} = new JsonObject(); } foreach ($value as $prop => $val) { foreach ($val as $v) { if (property_exists($v, '@value') || property_exists($v, '@list')) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_PROPERTY_VALUE, 'Detected invalid value in @reverse-map (only nodes are allowed', $v ); } self::mergeIntoProperty($element->{$keyword}, $prop, $v, true); } } return; } if ('@graph' === $keyword) { $this->expand($value, $activectx, $keyword, $frame); self::mergeIntoProperty($element, $keyword, $value, true); return; } } /** * Expands a scalar value * * @param mixed $value The value to expand. * @param array $activectx The active context. * @param string $activeprty The active property. * * @return JsonObject The expanded value. */ private function expandValue($value, $activectx, $activeprty) { $def = $this->getPropertyDefinition($activectx, $activeprty); $result = new JsonObject(); if ('@id' === $def['@type']) { $result->{'@id'} = $this->expandIri($value, $activectx, true); } elseif ('@vocab' === $def['@type']) { $result->{'@id'} = $this->expandIri($value, $activectx, true, true); } else { $result->{'@value'} = $value; if (isset($def['@type'])) { $result->{'@type'} = $def['@type']; } elseif (isset($def['@language']) && is_string($result->{'@value'})) { $result->{'@language'} = $def['@language']; } } return $result; } /** * Expands a JSON-LD IRI value (term, compact IRI, IRI) to an absolute * IRI and relabels blank nodes * * @param mixed $value The value to be expanded to an absolute IRI. * @param array $activectx The active context. * @param bool $relativeIri Specifies whether $value should be treated as * relative IRI against the base IRI or not. * @param bool $vocabRelative Specifies whether $value is relative to @vocab * if set or not. * @param null|JsonObject $localctx If the IRI is being expanded as part of context * processing, the current local context has to be * passed as well. * @param array $path A path of already processed terms to detect * circular dependencies * * @return string The expanded IRI. */ private function expandIri( $value, $activectx, $relativeIri = false, $vocabRelative = false, $localctx = null, $path = array() ) { if ((null === $value) || in_array($value, self::$keywords)) { return $value; } if ($localctx) { if (in_array($value, $path)) { throw new JsonLdException( JsonLdException::CYCLIC_IRI_MAPPING, 'Cycle in context definition detected: ' . join(' -> ', $path) . ' -> ' . $path[0], $localctx ); } else { $path[] = $value; if (count($path) >= self::CONTEXT_MAX_IRI_RECURSIONS) { throw new JsonLdException( JsonLdException::UNSPECIFIED, 'Too many recursions in term definition: ' . join(' -> ', $path) . ' -> ' . $path[0], $localctx ); } } if (isset($localctx->{$value})) { $nested = null; if (is_string($localctx->{$value})) { $nested = $localctx->{$value}; } elseif (isset($localctx->{$value}->{'@id'})) { $nested = $localctx->{$value}->{'@id'}; } if ($nested && (end($path) !== $nested)) { return $this->expandIri($nested, $activectx, false, true, $localctx, $path); } } } // Terms apply only for vocab-relative IRIs if ((true === $vocabRelative) && array_key_exists($value, $activectx)) { return $activectx[$value]['@id']; } if (false !== strpos($value, ':')) { list($prefix, $suffix) = explode(':', $value, 2); if (('_' === $prefix) || ('//' === substr($suffix, 0, 2))) { // Safety measure to prevent reassigned of, e.g., http:// // the "_" prefix is reserved for blank nodes and can't be expanded return $value; } if ($localctx) { $prefix = $this->expandIri($prefix, $activectx, false, true, $localctx, $path); // If prefix contains a colon, we have successfully expanded it if (false !== strpos($prefix, ':')) { return $prefix . $suffix; } } elseif (array_key_exists($prefix, $activectx)) { // compact IRI return $activectx[$prefix]['@id'] . $suffix; } } else { if ($vocabRelative && array_key_exists('@vocab', $activectx)) { return $activectx['@vocab'] . $value; } elseif (($relativeIri) && (null !== $activectx['@base'])) { return (string) $activectx['@base']->resolve($value); } } // can't expand it, return as is return $value; } /** * Compacts a JSON-LD document * * Attention: This method must be called with an expanded element, * otherwise it might not work. * * @param mixed $element A JSON-LD element to be compacted. * @param array $activectx The active context. * @param array $inversectx The inverse context. * @param null|string $activeprty The active property. * * @return mixed The compacted JSON-LD document. */ public function compact(&$element, $activectx = array(), $inversectx = array(), $activeprty = null) { if (is_array($element)) { $result = array(); foreach ($element as &$item) { $this->compact($item, $activectx, $inversectx, $activeprty); if (null !== $item) { $result[] = $item; } } if ($this->compactArrays && (1 === count($result))) { $element = $result[0]; } else { $element = $result; } return; } if (false === is_object($element)) { // element is already in compact form, nothing else to do return; } if (property_exists($element, '@value') || property_exists($element, '@id')) { $def = $this->getPropertyDefinition($activectx, $activeprty); $element = $this->compactValue($element, $def, $activectx, $inversectx); if (false === is_object($element)) { return; } } // Otherwise, compact all properties $properties = get_object_vars($element); ksort($properties); $inReverse = ('@reverse' === $activeprty); $element = new JsonObject(); foreach ($properties as $property => $value) { if (in_array($property, self::$keywords)) { if ('@id' === $property) { $value = $this->compactIri($value, $activectx, $inversectx); } elseif ('@type' === $property) { if (is_string($value)) { $value = $this->compactIri($value, $activectx, $inversectx, null, true); } else { foreach ($value as &$iri) { $iri = $this->compactIri($iri, $activectx, $inversectx, null, true); } if ($this->compactArrays && (1 === count($value))) { $value = $value[0]; } } } elseif (('@graph' === $property) || ('@list' === $property)) { $this->compact($value, $activectx, $inversectx, $property); if (false === is_array($value)) { $value = array($value); } } elseif ('@reverse' === $property) { $this->compact($value, $activectx, $inversectx, $property); // Move reverse properties out of the map into element foreach (get_object_vars($value) as $prop => $val) { if ($this->getPropertyDefinition($activectx, $prop, '@reverse')) { $alwaysArray = ('@set' === $this->getPropertyDefinition($activectx, $prop, '@container')); self::mergeIntoProperty($element, $prop, $val, $alwaysArray); unset($value->{$prop}); } } if (0 === count(get_object_vars($value))) { continue; // no properties left in the @reverse-map } } // Get the keyword alias from the inverse context if available $activeprty = (isset($inversectx[$property]['term'])) ? $inversectx[$property]['term'] : $property; self::setProperty($element, $activeprty, $value, JsonLdException::COLLIDING_KEYWORDS); // ... continue with next property continue; } // handle @null-objects as used in framing if (is_object($value) && property_exists($value, '@null')) { $activeprty = $this->compactIri($property, $activectx, $inversectx, null, true, $inReverse); if (false === property_exists($element, $activeprty)) { $element->{$activeprty} = null; } continue; } // Make sure that empty arrays are preserved if (0 === count($value)) { $activeprty = $this->compactIri($property, $activectx, $inversectx, null, true, $inReverse); self::mergeIntoProperty($element, $activeprty, $value); // ... continue with next property continue; } // Compact every item in value separately as they could map to different terms foreach ($value as $item) { $activeprty = $this->compactIri($property, $activectx, $inversectx, $item, true, $inReverse); $def = $this->getPropertyDefinition($activectx, $activeprty); if (in_array($def['@container'], array('@language', '@index'))) { if (false === property_exists($element, $activeprty)) { $element->{$activeprty} = new JsonObject(); } $def[$def['@container']] = $item->{$def['@container']}; $item = $this->compactValue($item, $def, $activectx, $inversectx); $this->compact($item, $activectx, $inversectx, $activeprty); self::mergeIntoProperty($element->{$activeprty}, $def[$def['@container']], $item); continue; } if (is_object($item)) { if (property_exists($item, '@list')) { $this->compact($item->{'@list'}, $activectx, $inversectx, $activeprty); if (false === is_array($item->{'@list'})) { $item->{'@list'} = array($item->{'@list'}); } if ('@list' === $def['@container']) { // a term can just hold one list if it has a @list container // (we don't support lists of lists) self::setProperty( $element, $activeprty, $item->{'@list'}, JsonLdException::COMPACTION_TO_LIST_OF_LISTS ); continue; // ... continue with next value } else { $result = new JsonObject(); $alias = $this->compactIri('@list', $activectx, $inversectx, null, true); $result->{$alias} = $item->{'@list'}; if (isset($item->{'@index'})) { $alias = $this->compactIri('@index', $activectx, $inversectx, null, true); $result->{$alias} = $item->{'@index'}; } $item = $result; } } else { $this->compact($item, $activectx, $inversectx, $activeprty); } } // Merge value back into resulting object making sure that value is always // an array if a container is set or compactArrays is set to false $asArray = ((false === $this->compactArrays) || (false === $def['compactArrays'])); self::mergeIntoProperty($element, $activeprty, $item, $asArray); } } } /** * Compacts a value * * The passed property definition must be an associative array * containing the following data: * * * @type => type IRI or null * @language => language code or null * @index => index string or null * @container => the container: @set, @list, @language, or @index * * * @param mixed $value The value to compact (arrays are not allowed!). * @param array $definition The active property's definition. * @param array $activectx The active context. * @param array $inversectx The inverse context. * * @return mixed The compacted value. */ private function compactValue($value, $definition, $activectx, $inversectx) { if ('@index' === $definition['@container']) { unset($value->{'@index'}); } $numProperties = count(get_object_vars($value)); // @id object if (property_exists($value, '@id')) { if (1 === $numProperties) { if ('@id' === $definition['@type']) { return $this->compactIri($value->{'@id'}, $activectx, $inversectx); } if ('@vocab' === $definition['@type']) { return $this->compactIri($value->{'@id'}, $activectx, $inversectx, null, true); } } return $value; } // @value object $criterion = (isset($value->{'@type'})) ? '@type' : null; $criterion = (isset($value->{'@language'})) ? '@language' : $criterion; if (null !== $criterion) { if ((2 === $numProperties) && ($value->{$criterion} === $definition[$criterion])) { return $value->{'@value'}; } return $value; } // the object has neither a @type nor a @language property // check the active property's definition if (is_string($value->{'@value'}) && (null !== $definition['@language'])) { // if the property is language tagged or there's a default language, // we can't compact the value if it is a string return $value; } // we can compact the value return (1 === $numProperties) ? $value->{'@value'} : $value; } /** * Compacts an absolute IRI (or aliases a keyword) * * If the IRI couldn't be compacted, the IRI is returned as is. * * @param mixed $iri The IRI to be compacted. * @param array $activectx The active context. * @param array $inversectx The inverse context. * @param mixed $value The value of the property to compact. * @param bool $vocabRelative If `true` is passed, this method tries * to convert the IRI to an IRI relative to * `@vocab`; otherwise, that fall back * mechanism is disabled. * @param bool $reverse Is the IRI used within a @reverse container? * * @return string Returns the compacted IRI on success; otherwise the * IRI is returned as is. */ private function compactIri($iri, $activectx, $inversectx, $value = null, $vocabRelative = false, $reverse = false) { if ((true === $vocabRelative) && array_key_exists($iri, $inversectx)) { if (null !== $value) { $valueProfile = $this->getValueProfile($value, $inversectx); $container = ('@list' === $valueProfile['@container']) ? array('@list', '@null') : array($valueProfile['@container'], '@set', '@null'); if (null === $valueProfile['typeLang']) { $typeOrLang = array('@null'); $typeOrLangValue = array('@null'); } else { $typeOrLang = array($valueProfile['typeLang'], '@null'); $typeOrLangValue = array(); if (true === $reverse) { $typeOrLangValue[] = '@reverse'; } if (('@type' === $valueProfile['typeLang']) && ('@id' === $valueProfile['typeLangValue'])) { array_push($typeOrLangValue, '@id', '@vocab', '@null'); } elseif (('@type' === $valueProfile['typeLang']) && ('@vocab' === $valueProfile['typeLangValue'])) { array_push($typeOrLangValue, '@vocab', '@id', '@null'); } else { $typeOrLangValue = array($valueProfile['typeLangValue'], '@null'); } } $result = $this->queryInverseContext($inversectx[$iri], $container, $typeOrLang, $typeOrLangValue); if (null !== $result) { return $result; } } elseif (isset($inversectx[$iri]['term'])) { return $inversectx[$iri]['term']; } } // Compact using @vocab if ($vocabRelative && isset($activectx['@vocab']) && (0 === strpos($iri, $activectx['@vocab'])) && (false !== ($vocabIri = substr($iri, strlen($activectx['@vocab'])))) && (false === isset($activectx[$vocabIri]))) { return $vocabIri; } // Try to compact to a compact IRI foreach ($inversectx as $termIri => $def) { $termIriLen = strlen($termIri); if (isset($def['term']) && (0 === strncmp($iri, $termIri, $termIriLen))) { $compactIri = substr($iri, $termIriLen); if (false !== $compactIri && '' !== $compactIri) { $compactIri = $def['term'] . ':' . $compactIri; if (false === isset($activectx[$compactIri]) || ((false === $vocabRelative) && ($iri === $activectx[$compactIri]['@id']))) { return $compactIri; } } } } // Last resort, convert to a relative IRI if ((false === $vocabRelative) && (null !== $activectx['@base'])) { return (string) $activectx['@base']->baseFor($iri); } // IRI couldn't be compacted, return as is return $iri; } /** * Verifies whether two JSON-LD subtrees are equal not * * Please note that two unlabeled blank nodes will never be equal by * definition. * * @param mixed $a The first subtree. * @param mixed $b The second subree. * * @return bool Returns true if the two subtrees are equal; otherwise * false. */ private static function subtreeEquals($a, $b) { if (gettype($a) !== gettype($b)) { return false; } if (is_scalar($a)) { return ($a === $b); } if (is_array($a)) { $len = count($a); if ($len !== count($b)) { return false; } // TODO Ignore order for sets? for ($i = 0; $i < $len; $i++) { if (false === self::subtreeEquals($a[$i], $b[$i])) { return false; } } return true; } if (!property_exists($a, '@id') && !property_exists($a, '@value') && !property_exists($a, '@list')) { // Blank nodes can never match as they can't be identified return false; } $properties = array_keys(get_object_vars($a)); if (count($properties) !== count(get_object_vars($b))) { return false; } foreach ($properties as $property) { if ((false === property_exists($b, $property)) || (false === self::subtreeEquals($a->{$property}, $b->{$property}))) { return false; } } return true; } /** * Calculates a value profile * * A value profile represent the schema of the value ignoring the * concrete value. It is an associative array containing the following * keys-value pairs: * * * `@container`: the container, defaults to `@set` * * `typeLang`: is set to `@type` for typed values or `@language` for * (language-tagged) strings; for all other values it is set to * `null` * * `typeLangValue`: set to the type of a typed value or the language * of a language-tagged string (`@null` for all other strings); for * all other values it is set to `null` * * @param JsonObject $value The value. * @param array $inversectx The inverse context. * * @return array The value profile. */ private function getValueProfile(JsonObject $value, $inversectx) { $valueProfile = array( '@container' => '@set', 'typeLang' => '@type', 'typeLangValue' => '@id' ); if (property_exists($value, '@index')) { $valueProfile['@container'] = '@index'; } if (property_exists($value, '@id')) { if (isset($inversectx[$value->{'@id'}]['term'])) { $valueProfile['typeLangValue'] = '@vocab'; } else { $valueProfile['typeLangValue'] = '@id'; } return $valueProfile; } if (property_exists($value, '@value')) { if (property_exists($value, '@type')) { $valueProfile['typeLang'] = '@type'; $valueProfile['typeLangValue'] = $value->{'@type'}; } elseif (property_exists($value, '@language')) { $valueProfile['typeLang'] = '@language'; $valueProfile['typeLangValue'] = $value->{'@language'}; if (false === property_exists($value, '@index')) { $valueProfile['@container'] = '@language'; } } else { $valueProfile['typeLang'] = '@language'; $valueProfile['typeLangValue'] = '@null'; } return $valueProfile; } if (property_exists($value, '@list')) { $len = count($value->{'@list'}); if ($len > 0) { $valueProfile = $this->getValueProfile($value->{'@list'}[0], $inversectx); } if (false === property_exists($value, '@index')) { $valueProfile['@container'] = '@list'; } for ($i = $len - 1; $i > 0; $i--) { $profile = $this->getValueProfile($value->{'@list'}[$i], $inversectx); if (($valueProfile['typeLang'] !== $profile['typeLang']) || ($valueProfile['typeLangValue'] !== $profile['typeLangValue'])) { $valueProfile['typeLang'] = null; $valueProfile['typeLangValue'] = null; return $valueProfile; } } } return $valueProfile; } /** * Queries the inverse context to find the term for a given query * path (= value profile) * * @param array $inversectx The inverse context (or a subtree thereof) * @param string[] $containers * @param string[] $typeOrLangs * @param string[] $typeOrLangValues * * @return null|string The best matching term or null if none was found. */ private function queryInverseContext($inversectx, $containers, $typeOrLangs, $typeOrLangValues) { foreach ($containers as $container) { foreach ($typeOrLangs as $typeOrLang) { foreach ($typeOrLangValues as $typeOrLangValue) { if (isset($inversectx[$container][$typeOrLang][$typeOrLangValue])) { return $inversectx[$container][$typeOrLang][$typeOrLangValue]; } } } } return null; } /** * Returns a property's definition * * The result will be in the form * * * array('@type' => type or null, * '@language' => language or null, * '@container' => container or null, * 'isKeyword' => true or false) * * * If `$only` is set, only the value of that key of the array * above will be returned. * * @param array $activectx The active context. * @param string $property The property. * @param null|string $only If set, only this element of the * definition will be returned. * * @return array|string|null Returns either the property's definition or * null if not found. */ private function getPropertyDefinition($activectx, $property, $only = null) { $result = array( '@reverse' => false, '@type' => null, '@language' => (isset($activectx['@language'])) ? $activectx['@language'] : null, '@index' => null, '@container' => null, 'isKeyword' => false, 'compactArrays' => true ); if (in_array($property, self::$keywords)) { $result['@type'] = (('@id' === $property) || ('@type' === $property)) ? '@id' : null; $result['@language'] = null; $result['isKeyword'] = true; $result['compactArrays'] = (bool) (('@list' !== $property) && ('@graph' !== $property)); } else { $def = (isset($activectx[$property])) ? $activectx[$property] : null; if (null !== $def) { $result['@id'] = $def['@id']; $result['@reverse'] = $def['@reverse']; if (isset($def['@type'])) { $result['@type'] = $def['@type']; $result['@language'] = null; } elseif (array_key_exists('@language', $def)) { // could be null $result['@language'] = $def['@language']; } if (isset($def['@container'])) { $result['@container'] = $def['@container']; if (('@list' === $def['@container']) || ('@set' === $def['@container'])) { $result['compactArrays'] = false; } } } } if ($only) { return (isset($result[$only])) ? $result[$only] : null; } return $result; } /** * Processes a local context to update the active context * * @param mixed $loclctx The local context. * @param array $activectx The active context. * @param array $remotectxs The already included remote contexts. * * @throws JsonLdException */ public function processContext($loclctx, &$activectx, $remotectxs = array()) { if (is_object($loclctx)) { $loclctx = clone $loclctx; } if (false === is_array($loclctx)) { $loclctx = array($loclctx); } foreach ($loclctx as $context) { if (null === $context) { $activectx = array('@base' => $this->baseIri); } elseif (is_object($context)) { // make sure we don't modify the passed context $context = clone $context; if (property_exists($context, '@base')) { if (count($remotectxs) > 0) { // do nothing, @base is ignored in a remote context } elseif (null === $context->{'@base'}) { $activectx['@base'] = null; } elseif (false === is_string($context->{'@base'})) { throw new JsonLdException( JsonLdException::INVALID_BASE_IRI, 'The value of @base must be an IRI or null.', $context ); } else { $base = new IRI($context->{'@base'}); if (false === $base->isAbsolute()) { if (null === $activectx['@base']) { throw new JsonLdException( JsonLdException::INVALID_BASE_IRI, 'The relative base IRI cannot be resolved to an absolute IRI.', $context ); } $activectx['@base'] = $activectx['@base']->resolve($base); } else { $activectx['@base'] = $base; } } unset($context->{'@base'}); } if (property_exists($context, '@vocab')) { if (null === $context->{'@vocab'}) { unset($activectx['@vocab']); } elseif ((false === is_string($context->{'@vocab'})) || (false === strpos($context->{'@vocab'}, ':'))) { throw new JsonLdException( JsonLdException::INVALID_VOCAB_MAPPING, 'The value of @vocab must be an absolute IRI or null.invalid vocab mapping, ', $context ); } else { $activectx['@vocab'] = $context->{'@vocab'}; } unset($context->{'@vocab'}); } if (property_exists($context, '@language')) { if ((null !== $context->{'@language'}) && (false === is_string($context->{'@language'}))) { throw new JsonLdException( JsonLdException::INVALID_DEFAULT_LANGUAGE, 'The value of @language must be a string.', $context ); } $activectx['@language'] = $context->{'@language'}; unset($context->{'@language'}); } foreach ($context as $key => $value) { unset($context->{$key}); unset($activectx[$key]); if (in_array($key, self::$keywords)) { throw new JsonLdException(JsonLdException::KEYWORD_REDEFINITION, null, $key); } if ((null === $value) || is_string($value)) { $value = (object) array('@id' => $value); } elseif (is_object($value)) { $value = clone $value; // make sure we don't modify context entries } else { throw new JsonLdException(JsonLdException::INVALID_TERM_DEFINITION); } if (property_exists($value, '@reverse')) { if (property_exists($value, '@id')) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_PROPERTY, "Invalid term definition using both @reverse and @id detected", $value ); } if (property_exists($value, '@container') && ('@index' !== $value->{'@container'}) && ('@set' !== $value->{'@container'})) { throw new JsonLdException( JsonLdException::INVALID_REVERSE_PROPERTY, "Terms using the @reverse feature support only @set- and @index-containers.", $value ); } $value->{'@id'} = $value->{'@reverse'}; $value->{'@reverse'} = true; } else { $value->{'@reverse'} = false; } if (property_exists($value, '@id')) { if ((null !== $value->{'@id'}) && (false === is_string($value->{'@id'}))) { throw new JsonLdException(JsonLdException::INVALID_IRI_MAPPING, null, $value->{'@id'}); } $path = array(); if ($key !== $value->{'@id'}) { $path[] = $key; } $expanded = $this->expandIri($value->{'@id'}, $activectx, false, true, $context, $path); if ($value->{'@reverse'} && (false === strpos($expanded, ':'))) { throw new JsonLdException( JsonLdException::INVALID_IRI_MAPPING, "Reverse properties must expand to absolute IRIs, \"$key\" expands to \"$expanded\"." ); } elseif ('@context' === $expanded) { throw new JsonLdException( JsonLdException::INVALID_KEYWORD_ALIAS, 'Aliases for @context are not supported', $value ); } } else { $expanded = $this->expandIri($key, $activectx, false, true, $context); } if ((null === $expanded) || in_array($expanded, self::$keywords)) { // if it's an aliased keyword or the IRI is null, we ignore all other properties // TODO Should we throw an exception if there are other properties? $activectx[$key] = array('@id' => $expanded, '@reverse' => false); continue; } elseif (false === strpos($expanded, ':')) { throw new JsonLdException( JsonLdException::INVALID_IRI_MAPPING, "Failed to expand \"$key\" to an absolute IRI.", $loclctx ); } $activectx[$key] = array('@id' => $expanded, '@reverse' => $value->{'@reverse'}); if (isset($value->{'@type'})) { if (false === is_string($value->{'@type'})) { throw new JsonLdException(JsonLdException::INVALID_TYPE_MAPPING); } $expanded = $this->expandIri($value->{'@type'}, $activectx, false, true, $context); if (('@id' !== $expanded) && ('@vocab' !== $expanded) && ((false === strpos($expanded, ':') || (0 === strpos($expanded, '_:'))))) { throw new JsonLdException( JsonLdException::INVALID_TYPE_MAPPING, "Failed to expand $expanded to an absolute IRI.", $loclctx ); } $activectx[$key]['@type'] = $expanded; } elseif (property_exists($value, '@language')) { if ((false === is_string($value->{'@language'})) && (null !== $value->{'@language'})) { throw new JsonLdException( JsonLdException::INVALID_LANGUAGE_MAPPING, 'The value of @language must be a string or null.', $value ); } // Note the else. Language tagging applies just to term without type coercion $activectx[$key]['@language'] = $value->{'@language'}; } if (isset($value->{'@container'})) { if (in_array($value->{'@container'}, array('@list', '@set', '@language', '@index'))) { $activectx[$key]['@container'] = $value->{'@container'}; } else { throw new JsonLdException( JsonLdException::INVALID_CONTAINER_MAPPING, 'A container mapping of ' . $value->{'@container'} . ' is not supported.' ); } } } } elseif (is_string($context)) { $remoteContext = new IRI($context); if ($remoteContext->isAbsolute()) { $remoteContext = (string) $remoteContext; } elseif (null === $activectx['@base']) { throw new JsonLdException( JsonLdException::INVALID_BASE_IRI, 'Can not resolve the relative URL of the remote context as no base has been set: ' . $remoteContext ); } else { $remoteContext = (string) $activectx['@base']->resolve($context); } if (in_array($remoteContext, $remotectxs)) { throw new JsonLdException( JsonLdException::RECURSIVE_CONTEXT_INCLUSION, 'Recursive inclusion of remote context: ' . join(' -> ', $remotectxs) . ' -> ' . $remoteContext ); } $remotectxs[] = $remoteContext; try { $remoteContext = $this->loadDocument($remoteContext); } catch (JsonLdException $e) { throw new JsonLdException( JsonLdException::LOADING_REMOTE_CONTEXT_FAILED, "Loading $remoteContext failed", null, null, $e ); } if (is_object($remoteContext) && property_exists($remoteContext, '@context')) { // TODO Use the context's IRI as base IRI when processing remote contexts (ISSUE-24) $this->processContext($remoteContext->{'@context'}, $activectx, $remotectxs); } else { throw new JsonLdException( JsonLdException::INVALID_REMOTE_CONTEXT, 'Remote context "' . $context . '" is invalid.', $remoteContext ); } } else { throw new JsonLdException(JsonLdException::INVALID_LOCAL_CONTEXT); } } } /** * Load a JSON-LD document * * The document can be supplied directly as string, by passing a file * path, or by passing a URL. * * @param null|string|array|JsonObject $input The JSON-LD document or a path * or URL pointing to one. * * @return mixed The loaded JSON-LD document * * @throws JsonLdException */ private function loadDocument($input) { if (false === is_string($input)) { // Return as is - it has already been parsed return $input; } $document = $this->documentLoader->loadDocument($input); return $document->document; } /** * Creates an inverse context to simplify IRI compaction * * The inverse context is a multidimensional array that has the * following shape: * * * [container|@null|term] * [@type|@language][typeIRI|languageCode] * [@null][@null] * [term|propGen] * [ array of terms ] * * * @param array $activectx The active context. * * @return array The inverse context. */ public function createInverseContext($activectx) { $inverseContext = array(); $defaultLanguage = isset($activectx['@language']) ? $activectx['@language'] : '@null'; $propertyGenerators = isset($activectx['@propertyGenerators']) ? $activectx['@propertyGenerators'] : array(); unset($activectx['@base']); unset($activectx['@vocab']); unset($activectx['@language']); unset($activectx['@propertyGenerators']); $activectx = array_merge($activectx, $propertyGenerators); unset($propertyGenerators); uksort($activectx, array($this, 'sortTerms')); // Put every IRI of each term into the inverse context foreach ($activectx as $term => $def) { if (null === $def['@id']) { // this is necessary since some terms can be decoupled from @vocab continue; } $container = (isset($def['@container'])) ? $def['@container'] : '@null'; $iri = $def['@id']; if (false === isset($inverseContext[$iri]['term']) && (false === $def['@reverse'])) { $inverseContext[$iri]['term'] = $term; } $typeOrLang = '@null'; $typeLangValue = '@null'; if (true === $def['@reverse']) { $typeOrLang = '@type'; $typeLangValue = '@reverse'; } elseif (isset($def['@type'])) { $typeOrLang = '@type'; $typeLangValue = $def['@type']; } elseif (array_key_exists('@language', $def)) { // can be null $typeOrLang = '@language'; $typeLangValue = (null === $def['@language']) ? '@null' : $def['@language']; } else { // Every untyped term is implicitly set to the default language if (false === isset($inverseContext[$iri][$container]['@language'][$defaultLanguage])) { $inverseContext[$iri][$container]['@language'][$defaultLanguage] = $term; } } if (false === isset($inverseContext[$iri][$container][$typeOrLang][$typeLangValue])) { $inverseContext[$iri][$container][$typeOrLang][$typeLangValue] = $term; } } // Sort the whole inverse context in reverse order, the longest IRI comes first uksort($inverseContext, array($this, 'sortTerms')); $inverseContext = array_reverse($inverseContext); return $inverseContext; } /** * Creates a node map of an expanded JSON-LD document * * All keys in the node map are prefixed with "-" to support empty strings. * * @param JsonObject $nodeMap The object holding the node map. * @param JsonObject|JsonObject[] $element An expanded JSON-LD element to * be put into the node map * @param string $activegraph The graph currently being processed. * @param null|string $activeid The node currently being processed. * @param null|string $activeprty The property currently being processed. * @param null|JsonObject $list The list object if a list is being * processed. */ private function generateNodeMap( &$nodeMap, $element, $activegraph = JsonLD::DEFAULT_GRAPH, $activeid = null, $activeprty = null, &$list = null ) { if (is_array($element)) { foreach ($element as $item) { $this->generateNodeMap($nodeMap, $item, $activegraph, $activeid, $activeprty, $list); } return; } // Relabel blank nodes in @type and add a node to the current graph if (property_exists($element, '@type')) { $types = null; if (is_array($element->{'@type'})) { $types = &$element->{'@type'}; } else { $types = array(&$element->{'@type'}); } foreach ($types as &$type) { if (0 === strncmp($type, '_:', 2)) { $type = $this->getBlankNodeId($type); } } } if (property_exists($element, '@value')) { // Handle value objects if (null === $list) { $this->mergeIntoProperty( $nodeMap->{'-' . $activegraph}->{'-' . $activeid}, $activeprty, $element, true, true ); } else { $this->mergeIntoProperty($list, '@list', $element, true, false); } } elseif (property_exists($element, '@list')) { // lists $result = new JsonObject(); $result->{'@list'} = array(); $this->generateNodeMap($nodeMap, $element->{'@list'}, $activegraph, $activeid, $activeprty, $result); $this->mergeIntoProperty( $nodeMap->{'-' . $activegraph}->{'-' . $activeid}, $activeprty, $result, true, false ); } else { // and node objects if (false === property_exists($element, '@id')) { $id = $this->getBlankNodeId(); } elseif (0 === strncmp($element->{'@id'}, '_:', 2)) { $id = $this->getBlankNodeId($element->{'@id'}); } else { $id = $element->{'@id'}; } unset($element->{'@id'}); // Create node in node map if it doesn't exist yet if (false === property_exists($nodeMap->{'-' . $activegraph}, '-' . $id)) { $node = new JsonObject(); $node->{'@id'} = $id; $nodeMap->{'-' . $activegraph}->{'-' . $id} = $node; } else { $node = $nodeMap->{'-' . $activegraph}->{'-' . $id}; } // Add reference to active property if (is_object($activeid)) { $this->mergeIntoProperty($node, $activeprty, $activeid, true, true); } elseif (null !== $activeprty) { $reference = new JsonObject(); $reference->{'@id'} = $id; if (null === $list) { $this->mergeIntoProperty( $nodeMap->{'-' . $activegraph}->{'-' . $activeid}, $activeprty, $reference, true, true ); } else { $this->mergeIntoProperty($list, '@list', $reference, true, false); } } if (property_exists($element, '@type')) { $this->mergeIntoProperty($node, '@type', $element->{'@type'}, true, true); unset($element->{'@type'}); } if (property_exists($element, '@index')) { $this->setProperty( $node, '@index', $element->{'@index'}, JsonLdException::CONFLICTING_INDEXES ); unset($element->{'@index'}); } if (property_exists($element, '@reverse')) { $reference = array('@id' => $id); // First, add the reverse property to all nodes pointing to this node and then // add them to the node mape foreach (get_object_vars($element->{'@reverse'}) as $property => $value) { foreach ($value as $val) { $this->generateNodeMap($nodeMap, $val, $activegraph, (object) $reference, $property); } } unset($element->{'@reverse'}); } // This node also represent a named graph, process it if (property_exists($element, '@graph')) { if (JsonLD::MERGED_GRAPH !== $activegraph) { if (false === property_exists($nodeMap, '-' . $id)) { $nodeMap->{'-' . $id} = new JsonObject(); } $this->generateNodeMap($nodeMap, $element->{'@graph'}, $id); } else { $this->generateNodeMap($nodeMap, $element->{'@graph'}, JsonLD::MERGED_GRAPH); } unset($element->{'@graph'}); } // Process all other properties in order $properties = get_object_vars($element); ksort($properties); foreach ($properties as $property => $value) { if (0 === strncmp($property, '_:', 2)) { $property = $this->getBlankNodeId($property); } if (false === property_exists($node, $property)) { $node->{$property} = array(); } $this->generateNodeMap($nodeMap, $value, $activegraph, $id, $property); } } } /** * Generate a new blank node identifier * * If an identifier is passed, a new blank node identifier is generated * for it and stored for subsequent use. Calling the method with the same * identifier (except null) will thus always return the same blank node * identifier. * * @param null|string $id If available, existing blank node identifier. * * @return string Returns a blank node identifier. */ private function getBlankNodeId($id = null) { if ((null !== $id) && isset($this->blankNodeMap[$id])) { return $this->blankNodeMap[$id]; } $bnode = '_:b' . $this->blankNodeCounter++; $this->blankNodeMap[$id] = $bnode; return $bnode; } /** * Flattens a JSON-LD document * * @param mixed $element A JSON-LD element to be flattened. * * @return array An array representing the flattened element. */ public function flatten($element) { $nodeMap = new JsonObject(); $nodeMap->{'-' . JsonLD::DEFAULT_GRAPH} = new JsonObject(); $this->generateNodeMap($nodeMap, $element); $defaultGraph = $nodeMap->{'-' . JsonLD::DEFAULT_GRAPH}; unset($nodeMap->{'-' . JsonLD::DEFAULT_GRAPH}); // Store named graphs in the @graph property of the node representing // the graph in the default graph foreach ($nodeMap as $graphName => $graph) { if (!isset($defaultGraph->{$graphName})) { $defaultGraph->{$graphName} = new JsonObject(); $defaultGraph->{$graphName}->{'@id'} = substr($graphName, 1); } $graph = (array) $graph; ksort($graph); $defaultGraph->{$graphName}->{'@graph'} = array_values( array_filter($graph, array($this, 'hasNodeProperties')) ); } $defaultGraph = (array) $defaultGraph; ksort($defaultGraph); return array_values( array_filter($defaultGraph, array($this, 'hasNodeProperties')) ); } /** * Converts an expanded JSON-LD document to RDF quads * * The result is an array of Quads. * * @param array $document The expanded JSON-LD document to be transformed into quads. * * @return Quad[] The extracted quads. */ public function toRdf(array $document) { $nodeMap = new JsonObject(); $nodeMap->{'-' . JsonLD::DEFAULT_GRAPH} = new JsonObject(); $this->generateNodeMap($nodeMap, $document); $result = array(); foreach ($nodeMap as $graphName => $graph) { $graphName = substr($graphName, 1); if (JsonLD::DEFAULT_GRAPH === $graphName) { $activegraph = null; } else { $activegraph = new IRI($graphName); if (false === $activegraph->isAbsolute()) { continue; } } foreach ($graph as $subject => $node) { $activesubj = new IRI(substr($subject, 1)); if (false === $activesubj->isAbsolute()) { continue; } foreach ($node as $property => $values) { if ('@id' === $property) { continue; } elseif ('@type' === $property) { $activeprty = new IRI(RdfConstants::RDF_TYPE); foreach ($values as $value) { $result[] = new Quad($activesubj, $activeprty, new IRI($value), $activegraph); } continue; } elseif ('@' === $property[0]) { continue; } // Exclude triples/quads with a blank node predicate if generalized RDF isn't enabled if ((0 === strncmp($property, '_:', 2)) && (false === $this->generalizedRdf)) { continue; } $activeprty = new IRI($property); if (false === $activeprty->isAbsolute()) { continue; } foreach ($values as $value) { if (property_exists($value, '@list')) { $quads = array(); $head = $this->listToRdf($value->{'@list'}, $quads, $activegraph); $result[] = new Quad($activesubj, $activeprty, $head, $activegraph); foreach ($quads as $quad) { $result[] = $quad; } } else { $object = $this->elementToRdf($value); if (null === $object) { continue; } $result[] = new Quad($activesubj, $activeprty, $object, $activegraph); } } } } } return $result; } /** * Converts a JSON-LD element to a RDF Quad object * * @param JsonObject $element The element to be converted. * * @return IRI|TypedValue|LanguageTagged|null The converted element to be used as Quad object. */ private function elementToRdf(JsonObject $element) { if (property_exists($element, '@value')) { return Value::fromJsonLd($element); } $iri = new IRI($element->{'@id'}); return $iri->isAbsolute() ? $iri : null; } /** * Converts a JSON-LD list to a linked RDF list (quads) * * @param array $entries The list entries * @param array $quads The array to be used to hold the linked list * @param null|IRI $graph The graph to be used in the constructed Quads * * @return IRI Returns the IRI of the head of the list */ private function listToRdf(array $entries, array &$quads, IRI $graph = null) { if (0 === count($entries)) { return new IRI(RdfConstants::RDF_NIL); } $head = new IRI($this->getBlankNodeId()); $quads[] = new Quad($head, new IRI(RdfConstants::RDF_FIRST), $this->elementToRdf($entries[0]), $graph); $bnode = $head; for ($i = 1, $len = count($entries); $i < $len; $i++) { $next = new IRI($this->getBlankNodeId()); $quads[] = new Quad($bnode, new IRI(RdfConstants::RDF_REST), $next, $graph); $object = $this->elementToRdf($entries[$i]); if (null !== $object) { $quads[] = new Quad($next, new IRI(RdfConstants::RDF_FIRST), $object, $graph); } $bnode = $next; } $quads[] = new Quad($bnode, new IRI(RdfConstants::RDF_REST), new IRI(RdfConstants::RDF_NIL), $graph); return $head; } /** * Converts an array of RDF quads to a JSON-LD document * * The resulting JSON-LD document will be in expanded form. * * @param Quad[] $quads The quads to convert * * @return array The JSON-LD document. * * @throws InvalidQuadException If the quad is invalid. */ public function fromRdf(array $quads) { $graphs = new JsonObject(); $graphs->{JsonLD::DEFAULT_GRAPH} = new JsonObject(); $usages = new JsonObject(); foreach ($quads as $quad) { $graphName = JsonLD::DEFAULT_GRAPH; if ($quad->getGraph()) { $graphName = (string) $quad->getGraph(); // Add a reference to this graph to the default graph if it // doesn't exist yet if (false === isset($graphs->{JsonLD::DEFAULT_GRAPH}->{$graphName})) { $graphs->{JsonLD::DEFAULT_GRAPH}->{$graphName} = self::objectToJsonLd($quad->getGraph()); } } if (false === isset($graphs->{$graphName})) { $graphs->{$graphName} = new JsonObject(); } $graph = $graphs->{$graphName}; // Subjects and properties are always IRIs (blank nodes are IRIs // as well): convert them to a string representation $subject = (string) $quad->getSubject(); $property = (string) $quad->getProperty(); $object = $quad->getObject(); // All nodes are stored in the node map if (false === isset($graph->{$subject})) { $graph->{$subject} = self::objectToJsonLd($quad->getSubject()); } $node = $graph->{$subject}; // ... as are all objects that are IRIs or blank nodes if (($object instanceof IRI) && (false === isset($graph->{(string) $object}))) { $graph->{(string) $object} = self::objectToJsonLd($object); } if (($property === RdfConstants::RDF_TYPE) && (false === $this->useRdfType) && ($object instanceof IRI)) { self::mergeIntoProperty($node, '@type', (string) $object, true, true); } else { $value = self::objectToJsonLd($object, $this->useNativeTypes); self::mergeIntoProperty($node, $property, $value, true, true); // If the object is an IRI or blank node it might be the // beginning of a list. Store a reference to its usage so // that we can replace it with a list object later if ($object instanceof IRI) { $objectStr = (string) $object; // Usages of rdf:nil are stored per graph, while... if (RdfConstants::RDF_NIL == $objectStr) { $graph->{$objectStr}->usages[] = array( 'node' => $node, 'prop' => $property, 'value' => $value); // references to other nodes are stored globally (blank nodes could be shared across graphs) } else { if (!isset($usages->{$objectStr})) { $usages->{$objectStr} = array(); } // Make sure that the same triple isn't counted multiple times // TODO Making $usages->{$objectStr} a set would make this code simpler $graphSubjectProperty = $graphName . '|' . $subject . '|' . $property; if (false === isset($usages->{$objectStr}[$graphSubjectProperty])) { $usages->{$objectStr}[$graphSubjectProperty] = array( 'graph' => $graphName, 'node' => $node, 'prop' => $property, 'value' => $value); } } } } } // Transform linked lists to @list objects $this->createListObjects($graphs, $usages); // Generate the resulting document starting with the default graph $document = array(); $nodes = get_object_vars($graphs->{JsonLD::DEFAULT_GRAPH}); ksort($nodes); foreach ($nodes as $id => $node) { // is it a named graph? if (isset($graphs->{$id})) { $node->{'@graph'} = array(); $graphNodes = get_object_vars($graphs->{$id}); ksort($graphNodes); foreach ($graphNodes as $graphNodeId => $graphNode) { // Only add the node when it has properties other than @id if (count(get_object_vars($graphNode)) > 1) { $node->{'@graph'}[] = $graphNode; } } } if (count(get_object_vars($node)) > 1) { $document[] = $node; } } return $document; } /** * Reconstruct @list arrays from linked list structures * * @param JsonObject $graphs The graph map * @param JsonObject $usages The global node usage map */ private function createListObjects($graphs, $usages) { foreach ($graphs as $graph) { if (false === isset($graph->{RdfConstants::RDF_NIL})) { continue; } $nil = $graph->{RdfConstants::RDF_NIL}; foreach ($nil->usages as $usage) { $u = $usage; $node = $u['node']; $prop = $u['prop']; $head = $u['value']; $list = array(); $listNodes = array(); while ((RdfConstants::RDF_REST === $prop) && (1 === count($usages->{$node->{'@id'}})) && property_exists($node, RdfConstants::RDF_FIRST) && property_exists($node, RdfConstants::RDF_REST) && (1 === count($node->{RdfConstants::RDF_FIRST})) && (1 === count($node->{RdfConstants::RDF_REST})) && ((3 === count(get_object_vars($node))) || // only @id, rdf:first & rdf:next ((4 === count(get_object_vars($node))) && // or an additional rdf:type = rdf:List property_exists($node, '@type') && ($node->{'@type'} === array(RdfConstants::RDF_LIST))) ) ) { $list[] = reset($node->{RdfConstants::RDF_FIRST}); $listNodes[] = $node->{'@id'}; $u = reset($usages->{$node->{'@id'}}); $node = $u['node']; $prop = $u['prop']; $head = $u['value']; if (0 !== strncmp($node->{'@id'}, '_:', 2)) { break; } }; // The list is nested in another list if (RdfConstants::RDF_FIRST === $prop) { // If it is empty, we can't do anything but keep the rdf:nil node if (RdfConstants::RDF_NIL === $head->{'@id'}) { continue; } // ... otherwise we keep the head and convert the rest to @list $head = $graph->{$head->{'@id'}}; $head = reset($head->{RdfConstants::RDF_REST}); array_pop($list); array_pop($listNodes); } unset($head->{'@id'}); $head->{'@list'} = array_reverse($list); foreach ($listNodes as $node) { unset($graph->{$node}); } } unset($nil->usages); } } /** * Frames a JSON-LD document according a supplied frame * * @param array|JsonObject $element A JSON-LD element to be framed. * @param mixed $frame The frame. * * @return array $result The framed element in expanded form. * * @throws JsonLdException */ public function frame($element, $frame) { if ((false === is_array($frame)) || (1 !== count($frame)) || (false === is_object($frame[0]))) { throw new JsonLdException( JsonLdException::UNSPECIFIED, 'The frame is invalid. It must be a single object.', $frame ); } $frame = $frame[0]; $options = new JsonObject(); $options->{'@embed'} = true; $options->{'@embedChildren'} = true; // TODO Change this as soon as the tests haven been updated foreach (self::$framingKeywords as $keyword) { if (property_exists($frame, $keyword)) { $options->{$keyword} = $frame->{$keyword}; unset($frame->{$keyword}); } elseif (false === property_exists($options, $keyword)) { $options->{$keyword} = false; } } $procOptions = new JsonObject(); $procOptions->base = (string) $this->baseIri; // TODO Check which base IRI to use $procOptions->compactArrays = $this->compactArrays; $procOptions->optimize = $this->optimize; $procOptions->useNativeTypes = $this->useNativeTypes; $procOptions->useRdfType = $this->useRdfType; $procOptions->produceGeneralizedRdf = $this->generalizedRdf; $procOptions->documentFactory = $this->documentFactory; $procOptions->documentLoader = $this->documentLoader; $processor = new Processor($procOptions); $graph = JsonLD::MERGED_GRAPH; if (property_exists($frame, '@graph')) { $graph = JsonLD::DEFAULT_GRAPH; } $nodeMap = new JsonObject(); $nodeMap->{'-' . $graph} = new JsonObject(); $processor->generateNodeMap($nodeMap, $element, $graph); // Sort the node map to ensure a deterministic output // TODO Move this to a separate function as basically the same is done in flatten()? $nodeMap = (array) $nodeMap; foreach ($nodeMap as &$nodes) { $nodes = (array) $nodes; ksort($nodes); $nodes = (object) $nodes; } $nodeMap = (object) $nodeMap; unset($processor); $result = array(); foreach ($nodeMap->{'-' . $graph} as $node) { $this->nodeMatchesFrame($node, $frame, $options, $nodeMap, $graph, $result); } return $result; } /** * Checks whether a node matches a frame or not. * * @param JsonObject $node The node. * @param null|JsonObject $frame The frame. * @param JsonObject $options The current framing options. * @param JsonObject $nodeMap The node map. * @param string $graph The currently used graph. * @param array $parent The parent to which matching results should be added. * @param array $path The path of already processed nodes. * * @return bool Returns true if the node matches the frame, otherwise false. */ private function nodeMatchesFrame($node, $frame, $options, $nodeMap, $graph, &$parent, $path = array()) { // TODO How should lists be handled? Is the @list required in the frame (current behavior) or not? // https://github.com/json-ld/json-ld.org/issues/110 // TODO Add support for '@omitDefault'? $filter = null; if (null !== $frame) { $filter = get_object_vars($frame); } $result = new JsonObject(); // Make sure that @id is always in the result if the node matches the filter if (property_exists($node, '@id')) { $result->{'@id'} = $node->{'@id'}; if ((null === $filter) && in_array($node->{'@id'}, $path)) { $parent[] = $result; return true; } $path[] = $node->{'@id'}; } // If no filter is specified, simply return the passed node - {} is a wildcard if ((null === $filter) || (0 === count($filter))) { // TODO What effect should @explicit have with a wildcard match? if (is_object($node)) { if ((true === $options->{'@embed'}) || (false === property_exists($node, '@id'))) { $this->addMissingNodeProperties($node, $options, $nodeMap, $graph, $result, $path); } $parent[] = $result; } else { $parent[] = $node; } return true; } foreach ($filter as $property => $validValues) { if (is_array($validValues) && (0 === count($validValues))) { if (property_exists($node, $property) || (('@graph' === $property) && isset($result->{'@id'}) && property_exists($nodeMap, $result->{'@id'}))) { return false; // [] says that the property must not exist but it does } continue; } // If the property does not exist or is empty if ((false === property_exists($node, $property)) || (is_array($node->{$property}) && 0 === count($node->{$property}))) { // first check if it's @graph and whether the referenced graph exists if ('@graph' === $property) { if (isset($result->{'@id'}) && property_exists($nodeMap, $result->{'@id'})) { $result->{'@graph'} = array(); $match = false; foreach ($nodeMap->{'-' . $result->{'@id'}} as $item) { foreach ($validValues as $validValue) { $match |= $this->nodeMatchesFrame( $item, $validValue, $options, $nodeMap, $result->{'@id'}, $result->{'@graph'} ); } } if (false === $match) { return false; } else { continue; // with next property } } else { // the referenced graph doesn't exist return false; } } // otherwise, look if we have a default value for it if (false === is_array($validValues)) { $validValues = array($validValues); } $defaultFound = false; foreach ($validValues as $validValue) { if (is_object($validValue) && property_exists($validValue, '@default')) { if (null === $validValue->{'@default'}) { $result->{$property} = new JsonObject(); $result->{$property}->{'@null'} = true; } else { $result->{$property} = (is_array($validValue->{'@default'})) ? $validValue->{'@default'} : array($validValue->{'@default'}); } $defaultFound = true; break; } } if (true === $defaultFound) { continue; } return false; // required property does not exist and no default value was found } // Check whether the values of the property match the filter $match = false; $result->{$property} = array(); if (false === is_array($validValues)) { if ($node->{$property} === $validValues) { $result->{$property} = $node->{$property}; continue; } else { return false; } } foreach ($validValues as $validValue) { if (is_object($validValue)) { // Extract framing options from subframe ($validValue is a subframe) $validValue = clone $validValue; $newOptions = clone $options; unset($newOptions->{'@default'}); foreach (self::$framingKeywords as $keyword) { if (property_exists($validValue, $keyword)) { $newOptions->{$keyword} = $validValue->{$keyword}; unset($validValue->{$keyword}); } } $nodeValues = $node->{$property}; if (false === is_array($nodeValues)) { $nodeValues = array($nodeValues); } foreach ($nodeValues as $value) { if (is_object($value) && property_exists($value, '@id')) { $match |= $this->nodeMatchesFrame( $nodeMap->{'-' . $graph}->{'-' . $value->{'@id'}}, $validValue, $newOptions, $nodeMap, $graph, $result->{$property}, $path ); } else { $match |= $this->nodeMatchesFrame( $value, $validValue, $newOptions, $nodeMap, $graph, $result->{$property}, $path ); } } } elseif (is_array($validValue)) { throw new JsonLdException( JsonLdException::UNSPECIFIED, "Invalid frame detected. Property \"$property\" must not be an array of arrays.", $frame ); } else { // This will just catch non-expanded IRIs for @id and @type $nodeValues = $node->{$property}; if (false === is_array($nodeValues)) { $nodeValues = array($nodeValues); } if (in_array($validValue, $nodeValues)) { $match = true; $result->{$property} = $node->{$property}; } } } if (false === $match) { return false; } } // Discard subtree if this object should not be embedded if ((false === $options->{'@embed'}) && property_exists($node, '@id')) { $result = new JsonObject(); $result->{'@id'} = $node->{'@id'}; $parent[] = $result; return true; } // all properties matched the filter, add the properties of the // node which haven't been added yet if (false === $options->{'@explicit'}) { $this->addMissingNodeProperties($node, $options, $nodeMap, $graph, $result, $path); } $parent[] = $result; return true; } /** * Adds all properties from node to result if they haven't been added yet * * @param JsonObject $node The node whose properties should processed. * @param JsonObject $options The current framing options. * @param JsonObject $nodeMap The node map. * @param string $graph The currently used graph. * @param JsonObject $result The object to which the properties should be added. * @param array $path The path of already processed nodes. */ private function addMissingNodeProperties($node, $options, $nodeMap, $graph, &$result, $path) { foreach ($node as $property => $value) { if (property_exists($result, $property)) { continue; // property has already been added } if (true === $options->{'@embedChildren'}) { if (false === is_array($value)) { $result->{$property} = unserialize(serialize($value)); // create a deep-copy continue; } $result->{$property} = array(); foreach ($value as $item) { if (is_object($item)) { if (property_exists($item, '@id')) { $item = $nodeMap->{'-' . $graph}->{'-' . $item->{'@id'}}; } $this->nodeMatchesFrame($item, null, $options, $nodeMap, $graph, $result->{$property}, $path); } else { $result->{$property}[] = $item; } } } else { // TODO Perform deep object copy?? $result->{$property} = unserialize(serialize($value)); // create a deep-copy } } } /** * Adds a property to an object if it doesn't exist yet * * If the property already exists, an exception is thrown as otherwise * the existing value would be lost. * * @param JsonObject $object The object. * @param string $property The name of the property. * @param mixed $value The value of the property. * * @throws JsonLdException If the property exists already JSON-LD. */ private static function setProperty(&$object, $property, $value, $errorCode = null) { if (property_exists($object, $property) && (false === self::subtreeEquals($object->{$property}, $value))) { if ($errorCode) { throw new JsonLdException( $errorCode, "Object already contains a property \"$property\".", $object ); } throw new JsonLdException( JsonLdException::UNSPECIFIED, "Object already contains a property \"$property\".", $object ); } $object->{$property} = $value; } /** * Merges a value into a property of an object * * @param JsonObject $object The object. * @param string $property The name of the property to which the value * should be merged into. * @param mixed $value The value to merge into the property. * @param bool $alwaysArray If set to true, the resulting property will * always be an array. * @param bool $unique If set to true, the value is only added if * it doesn't exist yet. */ private static function mergeIntoProperty(&$object, $property, $value, $alwaysArray = false, $unique = false) { // No need to add a null value if (null === $value) { return; } if (is_array($value)) { // Make sure empty arrays are created since we preserve them in expansion if ((0 === count($value)) && (false === property_exists($object, $property))) { $object->{$property} = array(); } foreach ($value as $val) { static::mergeIntoProperty($object, $property, $val, $alwaysArray, $unique); } return; } if (property_exists($object, $property)) { if (false === is_array($object->{$property})) { $object->{$property} = array($object->{$property}); } if ($unique) { foreach ($object->{$property} as $item) { if (self::subtreeEquals($item, $value)) { return; } } } $object->{$property}[] = $value; } else { $object->{$property} = ($alwaysArray) ? array($value) : $value; } } /** * Compares two values by their length and then lexicographically * * If two strings have different lengths, the shorter one will be * considered less than the other. If they have the same length, they * are compared lexicographically. * * @param mixed $a Value A. * @param mixed $b Value B. * * @return int If value A is shorter than value B, -1 will be returned; if it's * longer 1 will be returned. If both values have the same length * and value A is considered lexicographically less, -1 will be * returned, if they are equal 0 will be returned, otherwise 1 * will be returned. */ private static function sortTerms($a, $b) { $lenA = strlen($a); $lenB = strlen($b); if ($lenA < $lenB) { return -1; } elseif ($lenA === $lenB) { return strcmp($a, $b); } else { return 1; } } /** * Converts an object to a JSON-LD representation * * Only {@link IRI IRIs}, {@link LanguageTaggedString language-tagged strings}, * and {@link TypedValue typed values} are converted by this method. All * other objects are returned as-is. * * @param JsonObject $object The object to convert. * @param boolean $useNativeTypes If set to true, native types are used * for xsd:integer, xsd:double, and * xsd:boolean, otherwise typed strings * will be used instead. * * @return mixed The JSON-LD representation of the object. */ private static function objectToJsonLd($object, $useNativeTypes = true) { if ($object instanceof IRI) { $result = new JsonObject(); $result->{'@id'} = (string) $object; return $result; } elseif ($object instanceof Value) { return $object->toJsonLd($useNativeTypes); } return $object; } /** * Checks whether a node has properties and not just an @id * * This is used to filter nodes consisting just of an @id-member when * flattening and converting from RDF. * * @param JsonObject $node The node * * @return boolean True if the node has properties (other than @id), * false otherwise. */ private function hasNodeProperties($node) { return (count(get_object_vars($node)) > 1); } } JsonLD-1.2.1/Quad.php000066400000000000000000000054211431525543500142600ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use ML\IRI\IRI; /** * A quad * * @author Markus Lanthaler */ class Quad { /** * The subject * * @var IRI */ private $subject; /** * The property or predicate * * @var IRI */ private $property; /** * The object * * @var Value|IRI */ private $object; /** * The graph * * @var IRI */ private $graph; /** * Constructor * * @param IRI $subject The subject. * @param IRI $property The property. * @param Value|IRI $object The object. * @param null|IRI $graph The graph. * * @throws InvalidArgumentException If the object parameter has a wrong type */ public function __construct(IRI $subject, IRI $property, $object, IRI $graph = null) { $this->subject = $subject; $this->property = $property; $this->setObject($object); // use setter which checks the type $this->graph = $graph; } /** * Set the subject * * @param IRI $subject The subject */ public function setSubject(IRI $subject) { $this->subject = $subject; } /** * Get the subject * * @return IRI The subject */ public function getSubject() { return $this->subject; } /** * Set the property * * @param IRI $property The property */ public function setProperty(IRI $property) { $this->property = $property; } /** * Get the property * * @return IRI The property */ public function getProperty() { return $this->property; } /** * Set the object * * @param IRI|Value $object The object * * @throws InvalidArgumentException If object is of wrong type. */ public function setObject($object) { if (!($object instanceof IRI) && !($object instanceof Value)) { throw new \InvalidArgumentException('Object must be an IRI or Value object'); } $this->object = $object; } /** * Get the object * * @return IRI|Value The object */ public function getObject() { return $this->object; } /** * Set the graph * * @param null|IRI $graph The graph */ public function setGraph(IRI $graph = null) { $this->graph = $graph; } /** * Get the graph * * @return IRI The graph */ public function getGraph() { return $this->graph; } } JsonLD-1.2.1/QuadParserInterface.php000066400000000000000000000010341431525543500172520ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * Quad parser interface * * @author Markus Lanthaler */ interface QuadParserInterface { /** * Parses quads * * @param string $input The serialized quads to parse. * * @return Quad[] An array of extracted quads. */ public function parse($input); } JsonLD-1.2.1/QuadSerializerInterface.php000066400000000000000000000010721431525543500201310ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * Quad serializer interface * * @author Markus Lanthaler */ interface QuadSerializerInterface { /** * Serializes quads to a string. * * @param Quad[] $quads Array of quads to be serialized. * * @return string The serialized quads. */ public function serialize(array $quads); } JsonLD-1.2.1/README.md000066400000000000000000000065121431525543500141360ustar00rootroot00000000000000JsonLD [![Continuous integration](https://github.com/lanthaler/JsonLD/actions/workflows/ci.yaml/badge.svg)](https://github.com/lanthaler/JsonLD/actions/workflows/ci.yaml) ============== JsonLD is a fully conforming [JSON-LD](http://www.w3.org/TR/json-ld/) processor written in PHP. It is extensively tested and passes the [official JSON-LD test suite](https://github.com/json-ld/tests). There's an [online playground](http://www.markus-lanthaler.com/jsonld/playground/) where you can evaluate the processor's basic functionality. Additionally to the features defined by the [JSON-LD API specification](http://www.w3.org/TR/json-ld-api/), JsonLD supports [framing](http://json-ld.org/spec/latest/json-ld-framing/) (including [value matching](https://github.com/json-ld/json-ld.org/issues/110), [deep-filtering](https://github.com/json-ld/json-ld.org/issues/110), [aggressive re-embedding](https://github.com/json-ld/json-ld.org/issues/119), and [named graphs](https://github.com/json-ld/json-ld.org/issues/118)) and an experimental [object-oriented interface for JSON-LD documents](https://github.com/lanthaler/JsonLD/issues/15). Installation ------------ The easiest way to install `JsonLD` is by requiring it with [Composer](https://getcomposer.org/). ``` composer require ml/json-ld ``` ... and including Composer's autoloader to your project ```php require('vendor/autoload.php'); ``` Of course, you can also download JsonLD as [ZIP archive](https://github.com/lanthaler/JsonLD/releases) from Github. JsonLD requires PHP 5.3 or later. Usage ------------ The library supports the official [JSON-LD API](http://www.w3.org/TR/json-ld-api/) as well as a object-oriented interface for JSON-LD documents (not fully implemented yet, see [issue #15](https://github.com/lanthaler/JsonLD/issues/15) for details). All classes are extensively documented. Please have a look at the source code. ```php // Official JSON-LD API $expanded = JsonLD::expand('document.jsonld'); $compacted = JsonLD::compact('document.jsonld', 'context.jsonld'); $framed = JsonLD::frame('document.jsonld', 'frame.jsonld'); $flattened = JsonLD::flatten('document.jsonld'); $quads = JsonLD::toRdf('document.jsonld'); // Output the expanded document (pretty print) print JsonLD::toString($expanded, true); // Serialize the quads as N-Quads $nquads = new NQuads(); $serialized = $nquads->serialize($quads); print $serialized; // And parse them again to a JSON-LD document $quads = $nquads->parse($serialized); $document = JsonLD::fromRdf($quads); print JsonLD::toString($document, true); // Node-centric API $doc = JsonLD::getDocument('document.jsonld'); // get the default graph $graph = $doc->getGraph(); // get all nodes in the graph $nodes = $graph->getNodes(); // retrieve a node by ID $node = $graph->getNode('http://example.com/node1'); // get a property $node->getProperty('http://example.com/vocab/name'); // add a new blank node to the graph $newNode = $graph->createNode(); // link the new blank node to the existing node $node->addPropertyValue('http://example.com/vocab/link', $newNode); // even reverse properties are supported; this returns $newNode $node->getReverseProperty('http://example.com/vocab/link'); // serialize the graph and convert it to a string $serialized = JsonLD::toString($graph->toJsonLd()); ``` Commercial Support ------------ Commercial support is available on request. JsonLD-1.2.1/RdfConstants.php000066400000000000000000000017111431525543500157740ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * Some RDF constants. * * @author Markus Lanthaler */ abstract class RdfConstants { const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'; const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List'; const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first'; const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest'; const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'; const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer'; const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double'; const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean'; const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string'; } JsonLD-1.2.1/RemoteDocument.php000066400000000000000000000027601431525543500163230ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; /** * RemoteDocument * * @author Markus Lanthaler */ class RemoteDocument { /** * @var string The URL of the loaded document. */ public $documentUrl; /** * @var string The document's media type */ public $mediaType; /** * @var mixed The retrieved document. This can either be the raw payload * or the already parsed document. */ public $document; /** * @var string|null The value of the context Link header if available; * otherwise null. */ public $contextUrl; /** * Constructor * * @param null|string $documentUrl The final URL of the loaded document. * @param mixed $document The retrieved document (parsed or raw). * @param null|string $mediaType The document's media type. * @param null|string $contextUrl The value of the context Link header * if available; otherwise null. */ public function __construct($documentUrl = null, $document = null, $mediaType = null, $contextUrl = null) { $this->documentUrl = $documentUrl; $this->document = $document; $this->mediaType = $mediaType; $this->contextUrl = $contextUrl; } } JsonLD-1.2.1/Test/000077500000000000000000000000001431525543500135725ustar00rootroot00000000000000JsonLD-1.2.1/Test/DocumentTest.php000066400000000000000000000047471431525543500167350ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\JsonLD; use ML\JsonLD\Document; /** * Test the parsing of a JSON-LD document into a Document. * * @author Markus Lanthaler */ class DocumentTest extends \PHPUnit_Framework_TestCase { /** * The document instance being used throughout the tests. * * @var Document */ protected $document; /** * Create the document to test. */ protected function setUp() { $this->document = JsonLD::getDocument( dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR . 'dataset.jsonld', array('base' => 'http://example.com/dataset.jsonld') ); } /** * Tests whether all nodes are returned and blank nodes are renamed accordingly. */ public function testGetIri() { $this->assertEquals( 'http://example.com/dataset.jsonld', $this->document->getIri() ); } /** * Tests whether all nodes are interlinked correctly. */ public function testGetGraphNames() { // The blank node graph name _:_:graphBn gets relabeled to _:b0 during node map generation $this->assertEquals( array('_:b0', 'http://example.com/named-graph'), $this->document->getGraphNames() ); } /** * Tests whether all nodes also have the correct reverse links. */ public function testContainsGraph() { $this->assertTrue( $this->document->containsGraph('/named-graph'), 'Relative IRI' ); $this->assertTrue( $this->document->containsGraph('http://example.com/named-graph'), 'Absolute IRI' ); $this->assertTrue( $this->document->containsGraph('_:b0'), 'Blank node identifier' ); $this->assertFalse( $this->document->containsGraph('http://example.org/not-here'), 'Non-existent graph' ); } /** * Tests isBlankNode() */ public function testRemoveGraph() { $this->document->removeGraph('/named-graph'); $this->assertFalse( $this->document->containsGraph('/named-graph'), 'Is the removed graph still there?' ); } } JsonLD-1.2.1/Test/EarlReportGenerator.php000066400000000000000000000203571431525543500202400ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; /** * EarlReportGenerator * * A test listener to create an EARL report. It can be configured uses * the following configuration * * * * * * * ... * * * ... * * * ... * * * ... * * * ... * * * ... * * * ... * * * ... * * * ... * * * ... * * * * * * * @author Markus Lanthaler */ class EarlReportGenerator extends \PHPUnit_Util_Printer implements \PHPUnit_Framework_TestListener { /** * @var string */ protected $testTypeOfInterest = 'ML\\JsonLD\\Test\\W3CTestSuiteTest'; /** * @var array Lookup table for EARL statuses */ protected $earlStatuses; /** * @var array Options */ protected $options; /** * @var array Collected EARL assertions */ protected $assertions; /** * Constructor * * @param array $options Configuration options */ public function __construct(array $options = array()) { $reqOptions = array( 'target', 'project-name', 'project-url', 'project-homepage', 'license-url', 'project-description', 'programming-language', 'developer-name', 'developer-url', 'developer-homepage' ); foreach ($reqOptions as $option) { if (false === isset($options[$option])) { throw new \InvalidArgumentException( sprintf('The "%s" option is not set', $option) ); } } $this->options = $options; $this->earlStatuses = array( \PHPUnit_Runner_BaseTestRunner::STATUS_PASSED => 'earl:passed', \PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED => 'earl:untested', \PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE => 'earl:cantTell', \PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE => 'earl:failed', \PHPUnit_Runner_BaseTestRunner::STATUS_ERROR => 'earl:failed' ); $this->assertions = array(); parent::__construct($options['target']); } /** * A test ended. * * @param \PHPUnit_Framework_Test $test * @param float $time */ public function endTest(\PHPUnit_Framework_Test $test, $time) { if (false === ($test instanceof $this->testTypeOfInterest)) { return; } $assertion = array( '@type' => 'earl:Assertion', 'earl:assertedBy' => $this->options['developer-url'], 'earl:mode' => 'earl:automatic', 'earl:test' => $test->getTestId(), 'earl:result' => array( '@type' => 'earl:TestResult', 'earl:outcome' => $this->earlStatuses[$test->getStatus()], 'dc:date' => date('c') ) ); $this->assertions[] = $assertion; } /** * @inheritdoc */ public function flush() { if (0 === $this->assertions) { return; } $report = array( '@context' => array( 'doap' => 'http://usefulinc.com/ns/doap#', 'foaf' => 'http://xmlns.com/foaf/0.1/', 'dc' => 'http://purl.org/dc/terms/', 'earl' => 'http://www.w3.org/ns/earl#', 'xsd' => 'http://www.w3.org/2001/XMLSchema#', 'doap:homepage' => array('@type' => '@id'), 'doap:license' => array('@type' => '@id'), 'dc:creator' => array('@type' => '@id'), 'foaf:homepage' => array('@type' => '@id'), 'subjectOf' => array('@reverse' => 'earl:subject'), 'earl:assertedBy' => array('@type' => '@id'), 'earl:mode' => array('@type' => '@id'), 'earl:test' => array('@type' => '@id'), 'earl:outcome' => array('@type' => '@id'), 'dc:date' => array('@type' => 'xsd:date') ), '@id' => $this->options['project-url'], '@type' => array('doap:Project', 'earl:TestSubject', 'earl:Software'), 'doap:name' => $this->options['project-name'], 'dc:title' => $this->options['project-name'], 'doap:homepage' => $this->options['project-homepage'], 'doap:license' => $this->options['license-url'], 'doap:description' => $this->options['project-description'], 'doap:programming-language' => $this->options['programming-language'], 'doap:developer' => array( '@id' => $this->options['developer-url'], '@type' => array('foaf:Person', 'earl:Assertor'), 'foaf:name' => $this->options['developer-name'], 'foaf:homepage' => $this->options['developer-homepage'] ), 'dc:creator' => $this->options['developer-url'], 'dc:date' => array( '@value' => date('Y-m-d'), '@type' => 'xsd:date' ), 'subjectOf' => $this->assertions ); $options = 0; if (PHP_VERSION_ID >= 50400) { $options |= JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT; $report = json_encode($report, $options); } else { $report = json_encode($report); $report = str_replace('\\/', '/', $report); // unescape slahes // unescape unicode $report = preg_replace_callback( '/\\\\u([a-f0-9]{4})/', function ($match) { return iconv('UCS-4LE', 'UTF-8', pack('V', hexdec($match[1]))); }, $report ); } $this->write($report); parent::flush(); } /** * @inheritdoc */ public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) { } /** * @inheritdoc */ public function endTestSuite(\PHPUnit_Framework_TestSuite $suite) { } /** * @inheritdoc */ public function addError(\PHPUnit_Framework_Test $test, \Exception $e, $time) { } /** * @inheritdoc */ public function addFailure(\PHPUnit_Framework_Test $test, \PHPUnit_Framework_AssertionFailedError $e, $time) { } /** * @inheritdoc */ public function addIncompleteTest(\PHPUnit_Framework_Test $test, \Exception $e, $time) { } /** * @inheritdoc */ public function addSkippedTest(\PHPUnit_Framework_Test $test, \Exception $e, $time) { } /** * @inheritdoc */ public function startTest(\PHPUnit_Framework_Test $test) { } } JsonLD-1.2.1/Test/FileGetContentsLoaderTest.php000066400000000000000000000053651431525543500213400ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\IRI\IRI; use ML\JsonLD\FileGetContentsLoader; /** * Test the parsing of a JSON-LD document into a Document. */ class FileGetContentsLoaderTest extends \PHPUnit_Framework_TestCase { protected $iri; protected $loader; public function setUp() { parent::setUp(); $this->iri = new IRI('https://www.example.com'); $this->loader = new FileGetContentsLoader; } public function tearDown() { unset($iri); unset($this->loader); parent::tearDown(); } public function testParseLinkHeadersExactsValues() { $headers = array( '; param1=foo; param2="bar";', ); $parsed = $this->loader->parseLinkHeaders($headers, $this->iri); $this->assertEquals('https://www.example.com', $parsed[0]['uri']); $this->assertEquals('foo', $parsed[0]['param1']); $this->assertEquals('bar', $parsed[0]['param2']); } public function testParseLinkHeadersTrimsValues() { $headers = array( '< https://www.example.com >; param1= foo ; param2=" bar ";', ); $parsed = $this->loader->parseLinkHeaders($headers, $this->iri); $this->assertEquals('https://www.example.com', $parsed[0]['uri']); $this->assertEquals('foo', $parsed[0]['param1']); $this->assertEquals('bar', $parsed[0]['param2']); } public function testParseLinkHeadersWithMultipleHeaders() { $headers = array( '; param1=foo; param2=bar;', '; param1=fizz; param2=buzz;', ); $parsed = $this->loader->parseLinkHeaders($headers, $this->iri); $this->assertCount(2, $parsed); } public function testParseLinkHeadersWithMultipleLinks() { $headers = array( '; param1=foo; param2=bar;, ' . '; param1=fizz; param2=buzz;' ); $parsed = $this->loader->parseLinkHeaders($headers, $this->iri); $this->assertCount(2, $parsed); $this->assertEquals('https://www.example.com', $parsed[0]['uri']); $this->assertEquals('https://www.example.org', $parsed[1]['uri']); } public function testParseLinkHeadersConvertsRelativeLinksToAbsolute() { $headers = array(';'); $parsed = $this->loader->parseLinkHeaders($headers, $this->iri); $this->assertEquals('https://www.example.com/foo/bar', $parsed[0]['uri']); } } JsonLD-1.2.1/Test/Fixtures/000077500000000000000000000000001431525543500154035ustar00rootroot00000000000000JsonLD-1.2.1/Test/Fixtures/dataset.jsonld000066400000000000000000000013351431525543500202450ustar00rootroot00000000000000{ "@context": { "@vocab": "http://example.com/vocab#", "references": { "@type": "@id" } }, "@graph": [ { "@id": "/node1", "references": [ "_:graphBn", "/named-graph" ] }, { "@id": "_:graphBn", "@graph": [ { "@id": "_:graphBn/node1", "name": "_:graphBn/node1", "references": [ "_:bnode", "/node1", "/named-graph/node1" ] } ] }, { "@id": "/named-graph", "@graph": [ { "@id": "/named-graph/node1", "name": "/named-graph/node1", "references": [ "_:bnode", "/node1", "_:graphBn/node1" ] } ] }, { "@id": "_:bnode", "name": "_:bnode" } ] } JsonLD-1.2.1/Test/Fixtures/sample-compacted.jsonld000066400000000000000000000012701431525543500220340ustar00rootroot00000000000000{ "@context": { "t1": "http://example.com/t1", "t2": "http://example.com/t2", "term1": "http://example.com/term1", "term2": "http://example.com/term2", "term3": "http://example.com/term3", "term4": "http://example.com/term4", "term5": "http://example.com/term5" }, "@id": "http://example.com/id1", "@type": "t1", "term1": "v1", "term2": { "@value": "v2", "@type": "t2" }, "term3": { "@value": "v3", "@language": "en" }, "term4": 4, "term5": [ 50, 51 ], "http://example.com/term6": [ { "@value": "1", "@type": "t1" }, { "@value": "2", "@type": "t2" }, { "@value": "3", "@language": "en" }, { "@value": "4", "@language": "de" } ] } JsonLD-1.2.1/Test/Fixtures/sample-context.jsonld000066400000000000000000000004521431525543500215620ustar00rootroot00000000000000{ "@context": { "t1": "http://example.com/t1", "t2": "http://example.com/t2", "term1": "http://example.com/term1", "term2": "http://example.com/term2", "term3": "http://example.com/term3", "term4": "http://example.com/term4", "term5": "http://example.com/term5" } } JsonLD-1.2.1/Test/Fixtures/sample-expanded.jsonld000066400000000000000000000012621431525543500216660ustar00rootroot00000000000000[ { "@id": "http://example.com/id1", "@type": [ "http://example.com/t1" ], "http://example.com/term1": [ {"@value": "v1"} ], "http://example.com/term2": [ {"@value": "v2", "@type": "http://example.com/t2"} ], "http://example.com/term3": [ {"@value": "v3", "@language": "en"} ], "http://example.com/term4": [ {"@value": 4} ], "http://example.com/term5": [ { "@value": 50 }, { "@value": 51 } ], "http://example.com/term6": [ { "@value": "1", "@type": "http://example.com/t1" }, { "@value": "2", "@type": "http://example.com/t2" }, { "@value": "3", "@language": "en" }, { "@value": "4", "@language": "de" } ] } ] JsonLD-1.2.1/Test/Fixtures/sample-flattened.jsonld000066400000000000000000000014661431525543500220520ustar00rootroot00000000000000{ "@context": { "t1": "http://example.com/t1", "t2": "http://example.com/t2", "term1": "http://example.com/term1", "term2": "http://example.com/term2", "term3": "http://example.com/term3", "term4": "http://example.com/term4", "term5": "http://example.com/term5" }, "@graph": [ { "@id": "http://example.com/id1", "@type": "t1", "term1": "v1", "term2": { "@type": "t2", "@value": "v2" }, "term3": { "@language": "en", "@value": "v3" }, "term4": 4, "term5": [ 50, 51 ], "http://example.com/term6": [ { "@value": "1", "@type": "t1" }, { "@value": "2", "@type": "t2" }, { "@value": "3", "@language": "en" }, { "@value": "4", "@language": "de" } ] } ] } JsonLD-1.2.1/Test/Fixtures/sample-in.jsonld000066400000000000000000000012141431525543500205010ustar00rootroot00000000000000{ "@context": { "t1": "http://example.com/t1", "t2": "http://example.com/t2", "term1": "http://example.com/term1", "term3": "http://example.com/term3", "term5": "http://example.com/term5" }, "@id": "http://example.com/id1", "@type": "t1", "term1": "v1", "http://example.com/term2": { "@value": "v2", "@type": "t2" }, "term3": { "@value": "v3", "@language": "en" }, "http://example.com/term4": 4, "term5": [ 50, 51 ], "http://example.com/term6": [ { "@value": "1", "@type": "t1" }, { "@value": "2", "@type": "t2" }, { "@value": "3", "@language": "en" }, { "@value": "4", "@language": "de" } ] } JsonLD-1.2.1/Test/Fixtures/sample-serialized-document.jsonld000066400000000000000000000013341431525543500240450ustar00rootroot00000000000000[ { "@id": "http://example.com/id1", "@type": [ "http://example.com/t1" ], "http://example.com/term1": [ {"@value": "v1"} ], "http://example.com/term2": [ {"@value": "v2", "@type": "http://example.com/t2"} ], "http://example.com/term3": [ {"@value": "v3", "@language": "en"} ], "http://example.com/term4": [ {"@value": 4} ], "http://example.com/term5": [ {"@value": 50 }, {"@value": 51 } ], "http://example.com/term6": [ { "@value": "1", "@type": "http://example.com/t1" }, { "@value": "2", "@type": "http://example.com/t2" }, { "@value": "3", "@language": "en" }, { "@value": "4", "@language": "de" } ] }, { "@id": "http://example.com/t1" } ] JsonLD-1.2.1/Test/GraphTest.php000066400000000000000000001073001431525543500162050ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\Document; use ML\JsonLD\FileGetContentsLoader; use ML\JsonLD\Graph; use ML\JsonLD\GraphInterface; use ML\JsonLD\Node; use ML\JsonLD\LanguageTaggedString; use ML\JsonLD\TypedValue; use ML\JsonLD\RdfConstants; /** * Test the parsing of a JSON-LD document into a Graph. * * @author Markus Lanthaler */ class GraphTest extends \PHPUnit_Framework_TestCase { /** * The graph instance being used throughout the tests. * * @var GraphInterface */ protected $graph; /** * The document loader used to parse expected values. */ protected $documentLoader; /** * Create the graph to test. */ protected function setUp() { $json = << 'http://example.com/node/index.jsonld')); $this->graph = $doc->getGraph(); $this->documentLoader = new FileGetContentsLoader(); } /** * Tests whether all nodes are returned and blank nodes are renamed accordingly. */ public function testGetNodes() { $nodeIds = array( 'http://example.com/node/1', 'http://example.com/node/2', 'http://example.com/node/3', '_:b0', '_:b1', '_:b2', '_:b3', 'http://vocab.com/type/node', 'http://vocab.com/type/nodeWithAliases' ); $nodes = $this->graph->getNodes(); $this->assertCount(count($nodeIds), $nodes); foreach ($nodes as $node) { // Is the node's ID valid? $this->assertContains($node->getId(), $nodeIds, 'Found unexpected node ID: ' . $node->getId()); // Is the node of the right type? $this->assertInstanceOf('ML\JsonLD\Node', $node); // Does the graph return the same instance? $n = $this->graph->getNode($node->getId()); $this->assertSame($node, $n, 'same instance'); $this->assertTrue($node->equals($n), 'equals'); $this->assertSame($this->graph, $n->getGraph(), 'linked to graph'); } } /** * Tests whether all nodes are interlinked correctly. */ public function testNodeRelationships() { $node1 = $this->graph->getNode('http://example.com/node/1'); $node2 = $this->graph->getNode('http://example.com/node/2'); $node3 = $this->graph->getNode('http://example.com/node/3'); $node1_1 = $this->graph->getNode('_:b0'); $node2_1 = $this->graph->getNode('_:b1'); $node2_2 = $this->graph->getNode('_:b2'); $node3_1 = $this->graph->getNode('_:b3'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $nodeWithAliasesType = $this->graph->getNode('http://vocab.com/type/nodeWithAliases'); $this->assertSame($node2, $node1->getProperty('http://vocab.com/link'), 'n1 -link-> n2'); $this->assertSame($node1_1, $node1->getProperty('http://vocab.com/contains'), 'n1 -contains-> n1.1'); $this->assertSame($nodeType, $node1->getType(), 'n1 type'); $this->assertSame($node3, $node2->getProperty('http://vocab.com/link'), 'n2 -link-> n3'); $values = $node2->getProperty('http://vocab.com/contains'); $this->assertCount(2, $values, 'n2 -contains-> 2 nodes'); $this->assertSame($node2_1, $values[0], 'n2 -contains-> n2.1'); $this->assertSame($node2_2, $values[1], 'n2 -contains-> n2.1'); $this->assertSame($nodeWithAliasesType, $node2->getType(), 'n2 type'); $this->assertSame($node1, $node3->getProperty('http://vocab.com/link'), 'n3 -link-> n1'); $this->assertSame($node3_1, $node3->getProperty('http://vocab.com/contains'), 'n3 -contains-> n3.1'); $this->assertSame($nodeType, $node3->getType(), 'n3 type'); } /** * Tests whether all nodes also have the correct reverse links. */ public function testNodeReverseRelationships() { $node1 = $this->graph->getNode('http://example.com/node/1'); $node2 = $this->graph->getNode('http://example.com/node/2'); $node3 = $this->graph->getNode('http://example.com/node/3'); $node1_1 = $this->graph->getNode('_:b0'); $node2_1 = $this->graph->getNode('_:b1'); $node2_2 = $this->graph->getNode('_:b2'); $node3_1 = $this->graph->getNode('_:b3'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $nodeWithAliasesType = $this->graph->getNode('http://vocab.com/type/nodeWithAliases'); $this->assertSame($node1, $node2->getReverseProperty('http://vocab.com/link'), 'n2 <-link- n1'); $this->assertSame($node1, $node1_1->getReverseProperty('http://vocab.com/contains'), 'n1.1 <-contains- n1'); $this->assertSame($node2, $node3->getReverseProperty('http://vocab.com/link'), 'n3 <-link- n2'); $this->assertSame($node2, $node2_1->getReverseProperty('http://vocab.com/contains'), 'n2.1 <-contains- n2'); $this->assertSame($node2, $node2_2->getReverseProperty('http://vocab.com/contains'), 'n2.1 <-contains- n2'); $this->assertSame($node3, $node1->getReverseProperty('http://vocab.com/link'), 'n1 <-link- n3'); $this->assertSame($node3, $node3_1->getReverseProperty('http://vocab.com/contains'), 'n3.1 <-contains- n3'); $this->assertSame(array($node1, $node3), $nodeType->getReverseProperty(Node::TYPE), 'n1+n3 <-type- nodeType'); $this->assertSame(array($node2), $nodeWithAliasesType->getNodesWithThisType(), 'n2 <-type- nodeWithAliases'); } /** * Tests isBlankNode() */ public function testNodeIsBlankNode() { $this->assertFalse($this->graph->getNode('http://example.com/node/1')->isBlankNode(), 'n1'); $this->assertFalse($this->graph->getNode('http://example.com/node/2')->isBlankNode(), 'n2'); $this->assertFalse($this->graph->getNode('http://example.com/node/3')->isBlankNode(), 'n3'); $this->assertTrue($this->graph->getNode('_:b0')->isBlankNode(), '_:b0'); $this->assertTrue($this->graph->getNode('_:b1')->isBlankNode(), '_:b1'); $this->assertTrue($this->graph->getNode('_:b2')->isBlankNode(), '_:b2'); $this->assertTrue($this->graph->getNode('_:b3')->isBlankNode(), '_:b3'); $node = $this->graph->createNode(); $this->assertTrue($node->isBlankNode(), 'new node without ID'); $node = $this->graph->createNode('_:fljdf'); $this->assertTrue($node->isBlankNode(), 'new node blank node ID'); $node = $this->graph->createNode('http://www.example.com/node/new'); $this->assertFalse($node->isBlankNode(), 'new node with ID'); } /** * Tests if reverse node relationships are updated when a property is updated. */ public function testNodeReverseRelationshipsUpdated() { $node1 = $this->graph->getNode('http://example.com/node/1'); $node1_1 = $this->graph->getNode('_:b0'); $node2 = $this->graph->getNode('http://example.com/node/2'); $node3 = $this->graph->getNode('http://example.com/node/3'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $nodeWithAliasesType = $this->graph->getNode('http://vocab.com/type/nodeWithAliases'); $revProperties = $node2->getReverseProperties(); $this->assertCount(1, $revProperties, 'Check number of node2\'s reverse properties'); $this->assertSame( array('http://vocab.com/link' => array($node1)), $revProperties, 'Check node2\'s reverse properties' ); $node1->setProperty('http://vocab.com/link', null); $this->assertNull($node1->getProperty('http://vocab.com/link'), 'n1 -link-> n2 removed'); $node1->removePropertyValue('http://vocab.com/contains', $node1_1); $this->assertNull($node1->getProperty('http://vocab.com/contains'), 'n1 -contains-> n1.1 removed'); $this->assertNull($node2->getReverseProperty('http://vocab.com/link'), 'n2 <-link- n1 removed'); $this->assertNull($node1_1->getReverseProperty('http://vocab.com/contains'), 'n1.1 <-contains- n1 removed'); $expectedProperties = array( Node::TYPE => $this->graph->getNode('http://vocab.com/type/node'), 'http://vocab.com/name' => new TypedValue('1', RdfConstants::XSD_STRING) ); $properties = $node1->getProperties(); $this->assertCount(2, $properties, 'Check number of properties'); $this->assertEquals($expectedProperties, $properties, 'Check properties'); $this->assertSame(array($node1, $node3), $nodeType->getNodesWithThisType(), 'n1+n3 <-type- nodeType'); $this->assertSame($node2, $nodeWithAliasesType->getReverseProperty(Node::TYPE), 'n2 <-type- nodeWithAliases'); $node1->setType(null); $node2->removeType($nodeWithAliasesType); $this->assertSame($node3, $nodeType->getReverseProperty(Node::TYPE), 'n3 <-type- nodeType'); $this->assertSame(array(), $nodeWithAliasesType->getNodesWithThisType(), 'nodeWithAliases removed from n2'); } /** * Tests the removal of nodes from the graph. */ public function testNodeRemoval() { // Remove node 1 $node1 = $this->graph->getNode('/node/1'); $node1_1 = $this->graph->getNode('_:b0'); $node2 = $this->graph->getNode('http://example.com/node/2'); $this->assertTrue($this->graph->containsNode('http://example.com/node/1'), 'node 1 in graph?'); $this->assertSame( array('http://vocab.com/link' => array($node1)), $node2->getReverseProperties(), 'Check node2\'s reverse properties' ); $this->assertSame( array('http://vocab.com/contains' => array($node1)), $node1_1->getReverseProperties(), 'Check node1.1\'s reverse properties' ); $node1->removeFromGraph(); $this->assertSame(array(), $node2->getReverseProperties(), 'n2 reverse properties'); $this->assertNull($node2->getReverseProperty('http://vocab.com/link'), 'n2 <-link- n1 removed'); $this->assertSame(array(), $node1_1->getReverseProperties(), 'n1.1 reverse properties'); $this->assertNull($node1_1->getReverseProperty('http://vocab.com/contains'), 'n1.1 <-contains- n1 removed'); $this->assertFalse($this->graph->containsNode('/node/1'), 'node 1 still in graph?'); $this->assertNull($node1->getGraph(), 'node 1\'s graph reset?'); // Remove node 2 $node2 = $this->graph->getNode('http://example.com/node/2'); $node2_1 = $this->graph->getNode('_:b1'); $node2_2 = $this->graph->getNode('_:b2'); $node3 = $this->graph->getNode('/node/3'); $this->assertTrue($this->graph->containsNode('/node/2'), 'node 2 in graph?'); $this->assertSame( array('http://vocab.com/link' => array($node2)), $node3->getReverseProperties(), 'Check node3\'s reverse properties' ); $this->assertSame( array('http://vocab.com/contains' => array($node2)), $node2_1->getReverseProperties(), 'Check node2.1\'s reverse properties' ); $this->assertSame( array('http://vocab.com/contains' => array($node2)), $node2_2->getReverseProperties(), 'Check node2.2\'s reverse properties' ); $this->graph->removeNode($node2); $this->assertSame(array(), $node3->getReverseProperties(), 'n3 reverse properties'); $this->assertNull($node3->getReverseProperty('http://vocab.com/link'), 'n3 <-link- n2 removed'); $this->assertSame(array(), $node2_1->getReverseProperties(), 'n2.1 reverse properties'); $this->assertNull($node2_1->getReverseProperty('http://vocab.com/contains'), 'n2.1 <-contains- n2 removed'); $this->assertSame(array(), $node2_2->getReverseProperties(), 'n2.2 reverse properties'); $this->assertNull($node2_2->getReverseProperty('http://vocab.com/contains'), 'n2.2 <-contains- n2 removed'); $this->assertFalse($this->graph->containsNode('./2'), 'node 2 still in graph?'); } /** * Tests the removal of node types from the graph. */ public function testNodeTypeRemoval() { // Remove nodeType $node1 = $this->graph->getNode('http://example.com/node/1'); $node3 = $this->graph->getNode('/node/3'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $this->assertTrue($this->graph->containsNode('http://vocab.com/type/node'), 'node type in graph?'); $this->assertSame($nodeType, $node1->getType(), 'n1 type'); $this->assertSame($nodeType, $node3->getType(), 'n3 type'); $this->assertSame( array(Node::TYPE => array($node1, $node3)), $nodeType->getReverseProperties(), 'Check node type\'s reverse properties' ); $this->graph->removeNode($nodeType); $this->assertSame(array(), $nodeType->getReverseProperties(), 'node type\'s reverse properties'); $this->assertSame(array(), $nodeType->getNodesWithThisType(), 'n1+n3 <-type- node type removed'); $this->assertNull($node1->getType(), 'n1 type removed'); $this->assertNull($node3->getType(), 'n3 type removed'); $this->assertFalse($this->graph->containsNode('http://vocab.com/type/node'), 'node type still in graph?'); } /** * Tests if adding a value maintains uniqueness */ public function testNodePropertyUniqueness() { // Null $node = $this->graph->getNode('http://example.com/node/1'); $this->assertNull($node->getProperty('http://example.com/node/1'), 'inexistent'); $node->addPropertyValue('http://vocab.com/inexistent', null); $this->assertNull($node->getProperty('http://example.com/node/1'), 'inexistent + null'); $node->removeProperty('http://vocab.com/inexistent'); $node->removePropertyValue('http://vocab.com/inexistent', null); $this->assertNull($node->getProperty('http://example.com/node/1'), 'inexistent removed'); // Scalars $node = $this->graph->getNode('http://example.com/node/1'); $initialNameValue = $node->getProperty('http://vocab.com/name'); $this->assertEquals( new TypedValue('1', RdfConstants::XSD_STRING), $node->getProperty('http://vocab.com/name'), 'name: initial value' ); $node->addPropertyValue('http://vocab.com/name', '1'); $node->addPropertyValue('http://vocab.com/name', null); $this->assertSame($initialNameValue, $node->getProperty('http://vocab.com/name'), 'name: still same'); $node->addPropertyValue('http://vocab.com/name', 1); $this->assertEquals( array($initialNameValue, new TypedValue('1', RdfConstants::XSD_INTEGER)), $node->getProperty('http://vocab.com/name'), 'name: new value' ); $node->removePropertyValue('http://vocab.com/name', 1); $this->assertSame($initialNameValue, $node->getProperty('http://vocab.com/name'), 'name: removed new value'); // Language-tagged strings $node = $this->graph->getNode('http://example.com/node/2'); $value = $node->getProperty('http://vocab.com/lang'); $this->assertInstanceOf('ML\JsonLD\LanguageTaggedString', $value, 'lang: initial value type'); $this->assertEquals('language-tagged string', $value->getValue(), 'lang: initial value'); $this->assertEquals('en', $value->getLanguage(), 'lang: initial language'); $sameLangValue = new LanguageTaggedString('language-tagged string', 'en'); $this->assertTrue($value->equals($sameLangValue), 'lang: equals same'); $newLangValue1 = new LanguageTaggedString('language-tagged string', 'de'); $this->assertFalse($value->equals($newLangValue1), 'lang: equals new1'); $newLangValue2 = new LanguageTaggedString('other language-tagged string', 'en'); $this->assertFalse($value->equals($newLangValue2), 'lang: equals new2'); $node->addPropertyValue('http://vocab.com/lang', $sameLangValue); $this->assertSame($value, $node->getProperty('http://vocab.com/lang'), 'lang: still same'); $node->addPropertyValue('http://vocab.com/lang', $newLangValue1); $node->addPropertyValue('http://vocab.com/lang', $newLangValue2); $value = $node->getProperty('http://vocab.com/lang'); $this->assertCount(3, $value, 'lang: count values added'); $this->assertTrue($sameLangValue->equals($value[0]), 'lang: check values 1'); $this->assertTrue($newLangValue1->equals($value[1]), 'lang: check values 2'); $this->assertTrue($newLangValue2->equals($value[2]), 'lang: check values 3'); $node->removePropertyValue('http://vocab.com/lang', $newLangValue1); $value = $node->getProperty('http://vocab.com/lang'); $this->assertCount(2, $value, 'lang: count value 1 removed again'); $this->assertTrue($sameLangValue->equals($value[0]), 'lang: check values 1 (2)'); $this->assertTrue($newLangValue2->equals($value[1]), 'lang: check values 2 (2)'); // Typed values $node = $this->graph->getNode('http://example.com/node/2'); $value = $node->getProperty('http://vocab.com/typed'); $this->assertInstanceOf('ML\JsonLD\TypedValue', $value, 'typed: initial value class'); $this->assertEquals('typed value', $value->getValue(), 'typed: initial value'); $this->assertEquals('http://vocab.com/type/datatype', $value->getType(), 'typed: initial value type'); $sameTypedValue = new TypedValue('typed value', 'http://vocab.com/type/datatype'); $this->assertTrue($value->equals($sameTypedValue), 'typed: equals same'); $newTypedValue1 = new TypedValue('typed value', 'http://vocab.com/otherType'); $this->assertFalse($value->equals($newTypedValue1), 'typed: equals new1'); $newTypedValue2 = new TypedValue('other typed value', 'http://vocab.com/type/datatype'); $this->assertFalse($value->equals($newTypedValue2), 'typed: equals new2'); $node->addPropertyValue('http://vocab.com/typed', $sameTypedValue); $this->assertSame($value, $node->getProperty('http://vocab.com/typed'), 'typed: still same'); $node->addPropertyValue('http://vocab.com/typed', $newTypedValue1); $node->addPropertyValue('http://vocab.com/typed', $newTypedValue2); $value = $node->getProperty('http://vocab.com/typed'); $this->assertCount(3, $value, 'typed: count values added'); $this->assertTrue($sameTypedValue->equals($value[0]), 'typed: check values 1'); $this->assertTrue($newTypedValue1->equals($value[1]), 'typed: check values 2'); $this->assertTrue($newTypedValue2->equals($value[2]), 'typed: check values 3'); $node->removePropertyValue('http://vocab.com/typed', $newTypedValue1); $value = $node->getProperty('http://vocab.com/typed'); $this->assertCount(2, $value, 'typed: count value 1 removed again'); $this->assertTrue($sameTypedValue->equals($value[0]), 'typed: check values 1 (2)'); $this->assertTrue($newTypedValue2->equals($value[1]), 'typed: check values 2 (2)'); // Nodes $node = $this->graph->getNode('http://example.com/node/3'); $node1 = $this->graph->getNode('http://example.com/node/1'); $value = $node->getProperty('http://vocab.com/link'); $this->assertInstanceOf('ML\JsonLD\Node', $value, 'node: initial value class'); $this->assertSame($node1, $value, 'node: initial node'); $newNode1 = $this->graph->createNode(); $this->assertTrue($this->graph->containsNode($newNode1), 'node: new1 in graph'); $newNode2 = $this->graph->createNode('http://example.com/node/new/2'); $this->assertTrue($this->graph->containsNode($newNode2), 'node: new2 in graph'); $node->addPropertyValue('http://vocab.com/link', $node1); $this->assertSame($node1, $node->getProperty('http://vocab.com/link'), 'node: still same'); $node->addPropertyValue('http://vocab.com/link', $newNode1); $node->addPropertyValue('http://vocab.com/link', $newNode2); $value = $node->getProperty('http://vocab.com/link'); $this->assertCount(3, $value, 'node: count values added'); $this->assertSame($node1, $value[0], 'node: check values 1'); $this->assertSame($newNode1, $value[1], 'node: check values 2'); $this->assertSame($newNode2, $value[2], 'node: check values 3'); $node->removePropertyValue('http://vocab.com/link', $newNode1); $value = $node->getProperty('http://vocab.com/link'); $this->assertCount(2, $value, 'typed: count new node 1 removed again'); $this->assertTrue($node1->equals($value[0]), 'node: check values 1 (2)'); $this->assertTrue($newNode2->equals($value[1]), 'node: check values 2 (2)'); // Node types $node1 = $this->graph->getNode('http://example.com/node/1'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $nodeWithAliasesType = $this->graph->getNode('http://vocab.com/type/nodeWithAliases'); $this->assertSame($nodeType, $node1->getType(), 'type: n1 initial type'); $newType1 = $this->graph->createNode(); $this->assertTrue($this->graph->containsNode($newNode1), 'type: new1 in graph'); $node1->addType($nodeType); $this->assertSame($nodeType, $node1->getType(), 'type: n1 type still same'); $node1->addType($nodeWithAliasesType); $node1->addType($newType1); $value = $node1->getType(); $this->assertCount(3, $value, 'type: count values added'); $this->assertSame($nodeType, $value[0], 'type: check values 1'); $this->assertSame($nodeWithAliasesType, $value[1], 'type: check values 2'); $this->assertSame($newType1, $value[2], 'type: check values 3'); $node1->removeType($nodeWithAliasesType); $value = $node1->getType(); $this->assertCount(2, $value, 'typed: count nodeWithAliasesType removed again'); $this->assertTrue($nodeType->equals($value[0]), 'type: check values 1 (2)'); $this->assertTrue($newType1->equals($value[1]), 'type: check values 2 (2)'); } /** * Tests whether it is possible to add invalid values * * @expectedException InvalidArgumentException */ public function testAddInvalidPropertyValue() { $graph = new Graph(); $newNode = $graph->createNode(); $node1 = $this->graph->getNode('http://example.com/node/1'); $node1->addPropertyValue('http://vocab.com/link', $newNode); } /** * Tests whether it is possible to set the node's type to an invalid * value * * @expectedException InvalidArgumentException */ public function testSetInvalidTypeValue() { $node1 = $this->graph->getNode('http://example.com/node/1'); $node1->setType('http://vocab.com/type/aTypeAsString'); } /** * Tests whether it is possible to set the node's type to an invalid * value when an array is used. * * @expectedException InvalidArgumentException */ public function testSetInvalidTypeArray() { $types = array( $this->graph->getNode('http://vocab.com/type/nodeWithAliases'), 'http://vocab.com/type/aTypeAsString' ); $node1 = $this->graph->getNode('http://example.com/node/1'); $node1->setType($types); } /** * Tests whether it is possible to add an type which is not part of the * graph * * @expectedException InvalidArgumentException */ public function testAddTypeNotInGraph() { $graph = new Graph(); $newType = $graph->createNode(); $node1 = $this->graph->getNode('http://example.com/node/1'); $node1->addType($newType); } /** * Tests whether nodes are contained in the graph */ public function testContains() { $node1 = $this->graph->getNode('http://example.com/node/1'); $nodeb_0 = $this->graph->getNode('_:b0'); $this->assertTrue($this->graph->containsNode($node1), 'node1 obj'); $this->assertTrue($this->graph->containsNode('http://example.com/node/1'), 'node1 IRI'); $this->assertFalse($this->graph->containsNode('http://example.com/node/X'), 'inexistent IRI'); $this->assertTrue($this->graph->containsNode($nodeb_0), '_:b0'); $this->assertFalse($this->graph->containsNode('_:b0'), '_:b0 IRI'); $this->assertFalse($this->graph->containsNode(new TypedValue('val', 'http://example.com/type')), 'typed value'); } /** * Tests whether creating an existing node returns the instance of that node */ public function testCreateExistingNode() { $node1 = $this->graph->getNode('http://example.com/node/1'); $nodeType = $this->graph->getNode('http://vocab.com/type/node'); $this->assertSame($node1, $this->graph->createNode('http://example.com/node/1')); $this->assertSame($nodeType, $this->graph->createNode('http://vocab.com/type/node')); } /** * Tests the merging of two graphs */ public function testMerge() { $this->markTestSkipped("Merging graphs doesn't work yet as blank nodes are not relabeled properly"); $json = << 'http://example.com/node/index.jsonld'))->getGraph(); // Merge graph2 into graph $this->graph->merge($graph2); $nodeIds = array( 'http://example.com/node/1', 'http://example.com/node/2', 'http://example.com/node/3', 'http://example.com/node/4', '_:b0', '_:b1', '_:b2', '_:b3', '_:b4', 'http://vocab.com/type/node', 'http://vocab.com/type/nodeWithAliases' ); $nodes = $this->graph->getNodes(); $this->assertCount(count($nodeIds), $nodes); foreach ($nodes as $node) { // Is the node's ID valid? $this->assertContains($node->getId(), $nodeIds, 'Found unexpected node ID: ' . $node->getId()); // Is the node of the right type? $this->assertInstanceOf('ML\JsonLD\Node', $node); // Does the graph return the same instance? $n = $this->graph->getNode($node->getId()); $this->assertSame($node, $n, 'same instance'); $this->assertTrue($node->equals($n), 'equals'); $this->assertSame($this->graph, $n->getGraph(), 'linked to graph'); // It must not share node objects with graph 2 $this->assertNotSame($node, $graph2->getNode($node->getId()), 'shared instance between graph and graph 2'); } // Check that the properties have been updated as well $node1 = $this->graph->getNode('http://example.com/node/1'); $node2 = $this->graph->getNode('http://example.com/node/2'); $node3 = $this->graph->getNode('http://example.com/node/3'); $node4 = $this->graph->getNode('http://example.com/node/4'); $this->assertEquals( new TypedValue('1', RdfConstants::XSD_STRING), $node1->getProperty('http://vocab.com/name'), 'n1->name' ); $this->assertSame($node2, $node1->getProperty('http://vocab.com/link'), 'n1 -link-> n2'); $this->assertCount(2, $node1->getProperty('http://vocab.com/contains'), 'n1 -contains-> 2 blank nodes'); $this->assertEquals( array( new TypedValue('2', RdfConstants::XSD_STRING), new TypedValue('and a different name in graph 2', RdfConstants::XSD_STRING) ), $node2->getProperty('http://vocab.com/name'), 'n2->name' ); $this->assertSame(array($node3, $node4), $node2->getProperty('http://vocab.com/link'), 'n2 -link-> n3 & n4'); $this->assertEquals( new TypedValue('this was added in graph 2', RdfConstants::XSD_STRING), $node2->getProperty('http://vocab.com/newFromGraph2'), 'n2->newFromGraph2' ); $this->assertEquals( new TypedValue('node 4 from graph 2', RdfConstants::XSD_STRING), $node4->getProperty('http://vocab.com/name'), 'n4->name' ); // Verify that graph 2 wasn't changed $nodeIds = array( 'http://example.com/node/1', 'http://example.com/node/2', '_:b0', // ex:contains: { ex:nested } 'http://example.com/node/4', 'http://vocab.com/type/node' ); $nodes = $graph2->getNodes(); $this->assertCount(count($nodeIds), $nodes); foreach ($nodes as $node) { // Is the node's ID valid? $this->assertContains($node->getId(), $nodeIds, 'Found unexpected node ID in graph 2: ' . $node->getId()); // Is the node of the right type? $this->assertInstanceOf('ML\JsonLD\Node', $node); // Does the graph return the same instance? $n = $graph2->getNode($node->getId()); $this->assertSame($node, $n, 'same instance (graph 2)'); $this->assertTrue($node->equals($n), 'equals (graph 2)'); $this->assertSame($graph2, $n->getGraph(), 'linked to graph (graph 2)'); } } /** * Tests the serialization of nodes */ public function testSerializeNode() { $expected = $this->documentLoader->loadDocument( '{ "@id": "http://example.com/node/1", "@type": [ "http://vocab.com/type/node" ], "http://vocab.com/name": [ { "@value": "1" } ], "http://vocab.com/link": [ { "@id": "http://example.com/node/2" } ], "http://vocab.com/contains": [ { "@id": "_:b0" } ] }' ); $expected = $expected->document; $node1 = $this->graph->getNode('http://example.com/node/1'); $this->assertEquals($expected, $node1->toJsonLd(), 'Serialize node 1'); } /** * Tests the serialization of graphs */ public function testSerializeGraph() { // This is the expanded and flattened version of the test document // (the blank node labels have been renamed from _:t... to _:b...) $expected = $this->documentLoader->loadDocument( '[{ "@id": "_:b0", "http://vocab.com/nested": [{ "@value": "1.1" }] }, { "@id": "_:b1", "http://vocab.com/nested": [{ "@value": "2.1" }] }, { "@id": "_:b2", "http://vocab.com/nested": [{ "@value": "2.2" }] }, { "@id": "_:b3", "http://vocab.com/nested": [{ "@value": "3.1" }] }, { "@id": "http://example.com/node/1", "@type": ["http://vocab.com/type/node"], "http://vocab.com/contains": [{ "@id": "_:b0" }], "http://vocab.com/link": [{ "@id": "http://example.com/node/2" }], "http://vocab.com/name": [{ "@value": "1" }] }, { "@id": "http://example.com/node/2", "@type": ["http://vocab.com/type/nodeWithAliases"], "http://vocab.com/aliases": [{ "@value": "node2" }, { "@value": 2, "@type": "http://www.w3.org/2001/XMLSchema#integer" }], "http://vocab.com/contains": [{ "@id": "_:b1" }, { "@id": "_:b2" }], "http://vocab.com/lang": [{ "@language": "en", "@value": "language-tagged string" }], "http://vocab.com/link": [{ "@id": "http://example.com/node/3" }], "http://vocab.com/name": [{ "@value": "2" }], "http://vocab.com/typed": [{ "@type": "http://vocab.com/type/datatype", "@value": "typed value" }] }, { "@id": "http://example.com/node/3", "@type": ["http://vocab.com/type/node"], "http://vocab.com/contains": [{ "@id": "_:b3" }], "http://vocab.com/lang": [{ "@language": "en", "@value": "language-tagged string: en" }, { "@language": "de", "@value": "language-tagged string: de" }], "http://vocab.com/link": [{ "@id": "http://example.com/node/1" }], "http://vocab.com/name": [{ "@value": "3" }], "http://vocab.com/typed": [{ "@type": "http://vocab.com/type/datatype", "@value": "typed value" }, { "@language": "ex:/type/otherDataType", "@value": "typed value" }, { "@language": "ex:/type/datatype", "@value": "typed value" }] }, { "@id": "http://vocab.com/type/node" }, { "@id": "http://vocab.com/type/nodeWithAliases" }]' ); $expected = $expected->document; $this->assertEquals($expected, $this->graph->toJsonLd(false), 'Serialize graph'); } } JsonLD-1.2.1/Test/JsonLDApiTest.php000066400000000000000000000112311431525543500167240ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\JsonLD; /** * Tests JsonLD's API * * @author Markus Lanthaler */ class JsonLDApiTest extends JsonTestCase { /** * Tests the expansion API * * @group expansion */ public function testExpansion() { $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; $expected = json_decode(file_get_contents($path . 'sample-expanded.jsonld')); $input = $path . 'sample-in.jsonld'; $this->assertJsonEquals($expected, JsonLD::expand($input), 'Passing the file path'); $input = file_get_contents($input); $this->assertJsonEquals($expected, JsonLD::expand($input), 'Passing the raw input (string)'); $input = json_decode($input); $this->assertJsonEquals($expected, JsonLD::expand($input), 'Passing the parsed object'); } /** * Tests the compaction API * * @group compaction */ public function testCompaction() { $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; $expected = json_decode(file_get_contents($path . 'sample-compacted.jsonld')); $input = $path . 'sample-in.jsonld'; $context = $path . 'sample-context.jsonld'; $this->assertJsonEquals($expected, JsonLD::compact($input, $context), 'Passing the file path'); $input = file_get_contents($input); $context = file_get_contents($context); $this->assertJsonEquals($expected, JsonLD::compact($input, $context), 'Passing the raw input (string)'); $input = json_decode($input); $context = json_decode($context); $this->assertJsonEquals($expected, JsonLD::compact($input, $context), 'Passing the parsed object'); } /** * Tests the flattening API * * @group flattening */ public function testFlatten() { $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; $expected = json_decode(file_get_contents($path . 'sample-flattened.jsonld')); $input = $path . 'sample-in.jsonld'; $context = $path . 'sample-context.jsonld'; $this->assertJsonEquals($expected, JsonLD::flatten($input, $context), 'Passing the file path'); $input = file_get_contents($input); $context = file_get_contents($context); $this->assertJsonEquals($expected, JsonLD::flatten($input, $context), 'Passing the raw input (string)'); $input = json_decode($input); $context = json_decode($context); $this->assertJsonEquals($expected, JsonLD::flatten($input, $context), 'Passing the parsed object'); } /** * Tests the framing API * * This test intentionally uses the same fixtures as the flattening tests. * * @group framing */ public function testFrame() { $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; $expected = json_decode(file_get_contents($path . 'sample-flattened.jsonld')); $input = $path . 'sample-in.jsonld'; $context = $path . 'sample-context.jsonld'; $this->assertJsonEquals($expected, JsonLD::frame($input, $context), 'Passing the file path'); $input = file_get_contents($input); $context = file_get_contents($context); $this->assertJsonEquals($expected, JsonLD::frame($input, $context), 'Passing the raw input (string)'); $input = json_decode($input); $context = json_decode($context); $this->assertJsonEquals($expected, JsonLD::frame($input, $context), 'Passing the parsed object'); } /** * Tests the document API * * This test intentionally uses the same fixtures as the flattening tests. */ public function testGetDocument() { $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; $expected = json_decode(file_get_contents($path . 'sample-serialized-document.jsonld')); $input = $path . 'sample-in.jsonld'; $this->assertJsonEquals($expected, JsonLD::getDocument($input)->toJsonLd(), 'Passing the file path'); $input = file_get_contents($input); $this->assertJsonEquals($expected, JsonLD::getDocument($input)->toJsonLd(), 'Passing the raw input (string)'); $input = json_decode($input); $this->assertJsonEquals($expected, JsonLD::getDocument($input)->toJsonLd(), 'Passing the parsed object'); } } JsonLD-1.2.1/Test/JsonTestCase.php000066400000000000000000000032621431525543500166530ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; /** * A JSON Test Case * * This class extends {@link \PHPUnit_Framework_TestCase} with an assertion * to compare JSON. * * @author Markus Lanthaler */ abstract class JsonTestCase extends \PHPUnit_Framework_TestCase { /** * Asserts that two JSON structures are equal. * * @param object|array $expected * @param object|array $actual * @param string $message */ public static function assertJsonEquals($expected, $actual, $message = '') { $expected = self::normalizeJson($expected); $actual = self::normalizeJson($actual); self::assertEquals($expected, $actual, $message); } /** * Brings the keys of objects to a deterministic order to enable * comparison of JSON structures * * @param mixed $element The element to normalize. * * @return mixed The same data with all object keys ordered in a * deterministic way. */ private static function normalizeJson($element) { if (is_array($element)) { foreach ($element as &$item) { $item = self::normalizeJson($item); } } elseif (is_object($element)) { $element = (array) $element; ksort($element); $element = (object) $element; foreach ($element as &$item) { $item = self::normalizeJson($item); } } return $element; } } JsonLD-1.2.1/Test/NQuadsTest.php000066400000000000000000000057151431525543500163460ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\JsonLD; use ML\JsonLD\NQuads; /** * Tests NQuads * * @author Markus Lanthaler */ class NQuadsTest extends \PHPUnit_Framework_TestCase { /** * Tests that parsing an invalid NQuad file fails * * @expectedException \ML\JsonLD\Exception\InvalidQuadException */ public function testInvalidParse() { $nquads = new NQuads(); $nquads->parse('Invalid NQuads file'); } /** * Tests escaping */ public function testEscaping() { $doc = ''; $doc .= ' '; $doc .= ' "String with line-break \n and quote (\")" .'; $doc .= "\n"; $nquads = new NQuads(); $parsed = JsonLD::fromRdf($nquads->parse($doc)); $serialized = $nquads->serialize(JsonLD::toRdf($parsed)); $this->assertSame($doc, $serialized); } /** * Tests blank node label parsing */ public function testParseBlankNodes() { $nquads = new NQuads(); $this->assertNotEmpty($nquads->parse('_:b "Test" .'), 'just a letter'); $this->assertNotEmpty($nquads->parse('_:b1 "Test" .'), 'letter and number'); $this->assertNotEmpty($nquads->parse('_:_b1 "Test" .'), 'beginning _'); $this->assertNotEmpty($nquads->parse('_:b_1 "Test" .'), 'containing _'); $this->assertNotEmpty($nquads->parse('_:b1_ "Test" .'), 'ending _'); $this->assertNotEmpty($nquads->parse('_:b-1 "Test" .'), 'containing -'); $this->assertNotEmpty($nquads->parse('_:b-1 "Test" .'), 'ending -'); $this->assertNotEmpty($nquads->parse('_:b.1 "Test" .'), 'containing .'); } /** * Tests that parsing fails for blank node labels beginning with "-" * * @expectedException \ML\JsonLD\Exception\InvalidQuadException */ public function testParseBlankNodeDashAtTheBeginning() { $nquads = new NQuads(); $nquads->parse('_:-b1 "Test" .'); } /** * Tests that parsing fails for blank node labels beginning with "." * * @expectedException \ML\JsonLD\Exception\InvalidQuadException */ public function testParseBlankNodePeriodAtTheBeginning() { $nquads = new NQuads(); $nquads->parse('_:.b1 "Test" .'); } /** * Tests that parsing fails for blank node labels ending with "." * * @expectedException \ML\JsonLD\Exception\InvalidQuadException */ public function testParseBlankNodePeriodAtTheEnd() { $nquads = new NQuads(); $nquads->parse('_:b1. "Test" .'); } } JsonLD-1.2.1/Test/TestManifestIterator.php000066400000000000000000000057661431525543500204410ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; /** * TestManifestIterator reads a test manifest and returns the contained test * definitions. * * @author Markus Lanthaler */ class TestManifestIterator implements \Iterator { /** The current position. */ private $key = 0; /** The test directory. */ private $directory; /** The test manifest. */ private $manifest; /** The URL of the test manifest. */ private $url; /** The total number of tests. */ private $numberTests = 0; /** * Constructor * * @param string $file The manifest's filename. * @param string $url The manifest's URL. */ public function __construct($file, $url) { if (file_exists($file)) { $this->manifest = json_decode(file_get_contents($file)); $this->numberTests = count($this->manifest->{'sequence'}); $this->url = $url; $this->directory = dirname($file) . DIRECTORY_SEPARATOR; } } /** * Rewinds the TestManifestIterator to the first element. */ public function rewind() { $this->key = 0; } /** * Checks if current position is valid. * * @return bool True if the current position is valid; otherwise, false. */ public function valid() { return ($this->key < $this->numberTests); } /** * Returns the key of the current element. * * @return string The key of the current element */ public function key() { return $this->url . $this->manifest->{'sequence'}[$this->key]->{'@id'}; } /** * Returns the current element. * * @return array Returns an array containing the name of the test and the * test definition object. */ public function current() { $test = $this->manifest->{'sequence'}[$this->key]; $options = isset($test->{'option'}) ? clone $test->{'option'} // cloning because we are modifying it : new \stdClass(); if (false === property_exists($options, 'base')) { if (property_exists($this->manifest, 'baseIri')) { $options->base = $this->manifest->{'baseIri'} . $test->{'input'}; } else { $options->base = $test->{'input'}; } } if (isset($options->{'expandContext'}) && (false === strpos($options->{'expandContext'}, ':'))) { $options->{'expandContext'} = $this->directory . $options->{'expandContext'}; } $test = array( 'name' => $test->{'name'}, 'test' => $test, 'options' => $options ); return $test; } /** * Moves forward to next element. */ public function next() { $this->key++; } } JsonLD-1.2.1/Test/ValueTest.php000066400000000000000000000113101431525543500162130ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\LanguageTaggedString; use ML\JsonLD\TypedValue; /** * Test LanguageTaggedString and TypedValue * * @author Markus Lanthaler */ class ValueTest extends \PHPUnit_Framework_TestCase { /** * Tests LanguageTaggedString */ public function testLanguageTaggedString() { $string1 = new LanguageTaggedString('', ''); $this->assertSame('', $string1->getValue(), 'string1 value'); $this->assertSame('', $string1->getLanguage(), 'string1 language'); $string2 = new LanguageTaggedString('wert', 'de'); $this->assertSame('wert', $string2->getValue(), 'string2 value'); $this->assertSame('de', $string2->getLanguage(), 'string2 language'); $string3 = new LanguageTaggedString('value', 'en'); $this->assertSame('value', $string3->getValue(), 'string3 value'); $this->assertSame('en', $string3->getLanguage(), 'string3 language'); } /** * Tests LanguageTaggedString with an invalid value * * @expectedException \InvalidArgumentException */ public function testLanguageTaggedStringInvalidValue() { $string1 = new LanguageTaggedString('value', 'language'); $string1->setValue(1); } /** * Tests LanguageTaggedString with an invalid language * * @expectedException \InvalidArgumentException */ public function testLanguageTaggedStringInvalidLanguage() { $string1 = new LanguageTaggedString('value', 'language'); $string1->setLanguage(null); } /** * Tests TypedValue */ public function testTypedValue() { $value1 = new TypedValue('', ''); $this->assertSame('', $value1->getValue(), 'string1 value'); $this->assertSame('', $value1->getType(), 'string1 type'); $value2 = new TypedValue('wert', 'http://example.com/type1'); $this->assertSame('wert', $value2->getValue(), 'string2 value'); $this->assertSame('http://example.com/type1', $value2->getType(), 'string2 type'); $value3 = new TypedValue('value', 'http://example.com/type2'); $this->assertSame('value', $value3->getValue(), 'string3 value'); $this->assertSame('http://example.com/type2', $value3->getType(), 'string3 type'); } /** * Tests TypedValue with an invalid value * * @expectedException \InvalidArgumentException */ public function testTypedValueInvalidValue() { $value1 = new LanguageTaggedString('value', 'language'); $value1->setValue(1); } /** * Tests TypedValue with an invalid type * * @expectedException \InvalidArgumentException */ public function testTypedValueInvalidLanguage() { $value1 = new TypedValue('value', 'http://example.com/type'); $value1->setType(1); } /** * Tests TypedValue with an invalid type */ public function testEquals() { $string1a = new LanguageTaggedString('value', 'en'); $string1b = new LanguageTaggedString('value', 'en'); $string2 = new LanguageTaggedString('value', 'de'); $string3 = new LanguageTaggedString('wert', 'en'); $this->assertTrue($string1a->equals($string1b), 's1a == s1b?'); $this->assertFalse($string1a->equals($string2), 's1a == s2?'); $this->assertFalse($string1a->equals($string3), 's1a == s3?'); $this->assertFalse($string1b->equals($string2), 's1b == s2?'); $this->assertFalse($string1b->equals($string3), 's1b == s3?'); $this->assertFalse($string2->equals($string3), 's2 == s3?'); $typed1a = new TypedValue('value', 'http://example.com/type1'); $typed1b = new TypedValue('value', 'http://example.com/type1'); $typed2 = new TypedValue('value', 'http://example.com/type2'); $typed3 = new TypedValue('wert', 'http://example.com/type1'); $this->assertTrue($typed1a->equals($typed1b), 't1a == t1b?'); $this->assertFalse($typed1a->equals($typed2), 't1a == t2?'); $this->assertFalse($typed1a->equals($typed3), 't1a == t3?'); $this->assertFalse($typed1b->equals($typed2), 't1b == t2?'); $this->assertFalse($typed1b->equals($typed3), 't1b == t3?'); $this->assertFalse($typed2->equals($typed3), 't2 == t3?'); $string4 = new LanguageTaggedString('', ''); $typed4 = new TypedValue('', ''); $this->assertFalse($string4->equals($typed4), 's4 == t4'); $this->assertFalse($typed4->equals($string4), 's4 == t4'); } } JsonLD-1.2.1/Test/W3CTestSuiteTest.php000066400000000000000000000245241431525543500174200ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD\Test; use ML\JsonLD\JsonLD; use ML\JsonLD\NQuads; use ML\JsonLD\Test\TestManifestIterator; /** * The official W3C JSON-LD test suite. * * @link http://www.w3.org/2013/json-ld-tests/ Official W3C JSON-LD test suite * * @author Markus Lanthaler */ class W3CTestSuiteTest extends JsonTestCase { /** * The base directory from which the test manifests, input, and output * files should be read. */ private $basedir; /** * The URL corresponding to the base directory */ private $baseurl = 'http://json-ld.org/test-suite/tests/'; /** * @var string The test's ID. */ private $id; /** * Constructs a test case with the given name. * * @param null|string $name * @param array $data * @param string $dataName */ public function __construct($name = null, array $data = array(), $dataName = '') { $this->id = $dataName; parent::__construct($name, $data, $dataName); $this->basedir = dirname(__FILE__) . '/../vendor/json-ld/tests/'; } /** * Returns the test identifier. * * @return string The test identifier */ public function getTestId() { return $this->id; } /** * Tests expansion. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group expansion * @dataProvider expansionProvider */ public function testExpansion($name, $test, $options) { $expected = json_decode(file_get_contents($this->basedir . $test->{'expect'})); $result = JsonLD::expand($this->basedir . $test->{'input'}, $options); $this->assertJsonEquals($expected, $result); } /** * Provides expansion test cases. */ public function expansionProvider() { return new TestManifestIterator( $this->basedir . 'expand-manifest.jsonld', $this->baseurl . 'expand-manifest.jsonld' ); } /** * Tests compaction. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group compaction * @dataProvider compactionProvider */ public function testCompaction($name, $test, $options) { $expected = json_decode(file_get_contents($this->basedir . $test->{'expect'})); $result = JsonLD::compact( $this->basedir . $test->{'input'}, $this->basedir . $test->{'context'}, $options ); $this->assertJsonEquals($expected, $result); } /** * Provides compaction test cases. */ public function compactionProvider() { return new TestManifestIterator( $this->basedir . 'compact-manifest.jsonld', $this->baseurl . 'compact-manifest.jsonld' ); } /** * Tests flattening. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group flattening * @dataProvider flattenProvider */ public function testFlatten($name, $test, $options) { $expected = json_decode(file_get_contents($this->basedir . $test->{'expect'})); $context = (isset($test->{'context'})) ? $this->basedir . $test->{'context'} : null; $result = JsonLD::flatten($this->basedir . $test->{'input'}, $context, $options); $this->assertJsonEquals($expected, $result); } /** * Provides flattening test cases. */ public function flattenProvider() { return new TestManifestIterator( $this->basedir . 'flatten-manifest.jsonld', $this->baseurl . 'flatten-manifest.jsonld' ); } /** * Tests remote document loading. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group remote * @dataProvider remoteDocumentLoadingProvider */ public function testRemoteDocumentLoading($name, $test, $options) { if (in_array('jld:NegativeEvaluationTest', $test->{'@type'})) { $this->setExpectedException('ML\JsonLD\Exception\JsonLdException', null, $test->{'expect'}); } else { $expected = json_decode($this->replaceBaseUrl(file_get_contents($this->basedir . $test->{'expect'}))); } unset($options->base); $result = JsonLD::expand($this->replaceBaseUrl($this->baseurl . $test->{'input'}), $options); if (isset($expected)) { $this->assertJsonEquals($expected, $result); } } /** * Provides remote document loading test cases. */ public function remoteDocumentLoadingProvider() { return new TestManifestIterator( $this->basedir . 'remote-doc-manifest.jsonld', $this->baseurl . 'remote-doc-manifest.jsonld' ); } /** * Replaces the base URL 'http://json-ld.org/' with 'https://json-ld.org:443/'. * * The test location of the test suite has been changed as the site has been * updated to use HTTPS everywhere. * * @param string $input The input string. * * @return string The input string with all occurrences of the old base URL replaced with the new HTTPS-based one. */ private function replaceBaseUrl($input) { return str_replace('http://json-ld.org/', 'https://json-ld.org:443/', $input); } /** * Tests errors (uses flattening). * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group errors * @dataProvider errorProvider */ public function testError($name, $test, $options) { $this->setExpectedException('ML\JsonLD\Exception\JsonLdException', null, $test->{'expect'}); JsonLD::flatten( $this->basedir . $test->{'input'}, (isset($test->{'context'})) ? $this->basedir . $test->{'context'} : null, $options ); } /** * Provides error test cases. */ public function errorProvider() { return new TestManifestIterator( $this->basedir . 'error-manifest.jsonld', $this->baseurl . 'error-manifest.jsonld' ); } /** * Tests framing. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group framing * @dataProvider framingProvider */ public function testFraming($name, $test, $options) { $ignoredTests = array( 'frame-0005-in.jsonld', 'frame-0009-in.jsonld', 'frame-0010-in.jsonld', 'frame-0012-in.jsonld', 'frame-0013-in.jsonld', 'frame-0023-in.jsonld', 'frame-0024-in.jsonld', 'frame-0027-in.jsonld', 'frame-0028-in.jsonld', 'frame-0029-in.jsonld', 'frame-0030-in.jsonld' ); if (in_array($test->{'input'}, $ignoredTests)) { $this->markTestSkipped( 'This implementation uses deep value matching and aggressive re-embedding. See ISSUE-110 and ISSUE-119.' ); } $expected = json_decode(file_get_contents($this->basedir . $test->{'expect'})); $result = JsonLD::frame( $this->basedir . $test->{'input'}, $this->basedir . $test->{'frame'}, $options ); $this->assertJsonEquals($expected, $result); } /** * Provides framing test cases. */ public function framingProvider() { return new TestManifestIterator( $this->basedir . 'frame-manifest.jsonld', $this->baseurl . 'frame-manifest.jsonld' ); } /** * Tests conversion to RDF quads. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group toRdf * @dataProvider toRdfProvider */ public function testToRdf($name, $test, $options) { $expected = trim(file_get_contents($this->basedir . $test->{'expect'})); $quads = JsonLD::toRdf($this->basedir . $test->{'input'}, $options); $serializer = new NQuads(); $result = $serializer->serialize($quads); // Sort quads (the expected quads are already sorted) $result = explode("\n", trim($result)); sort($result); $result = implode("\n", $result); $this->assertEquals($expected, $result); } /** * Provides conversion to RDF quads test cases. */ public function toRdfProvider() { return new TestManifestIterator( $this->basedir . 'toRdf-manifest.jsonld', $this->baseurl . 'toRdf-manifest.jsonld' ); } /** * Tests conversion from quads. * * @param string $name The test name. * @param object $test The test definition. * @param object $options The options to configure the algorithms. * * @group fromRdf * @dataProvider fromRdfProvider */ public function testFromRdf($name, $test, $options) { $expected = json_decode(file_get_contents($this->basedir . $test->{'expect'})); $parser = new NQuads(); $quads = $parser->parse(file_get_contents($this->basedir . $test->{'input'})); $result = JsonLD::fromRdf($quads, $options); $this->assertEquals($expected, $result); } /** * Provides conversion to quads test cases. */ public function fromRdfProvider() { return new TestManifestIterator( $this->basedir . 'fromRdf-manifest.jsonld', $this->baseurl . 'fromRdf-manifest.jsonld' ); } } JsonLD-1.2.1/Test/bootstrap.php000066400000000000000000000011451431525543500163210ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; /** * A typed value represents a value with an associated type. * * @author Markus Lanthaler */ final class TypedValue extends Value { /** * The type of the value in the form of an IRI. * * @var string */ private $type; /** * Constructor * * @param string $value The value. * @param string $type The type. */ public function __construct($value, $type) { $this->setValue($value); $this->setType($type); } /** * Set the type * * For the sake of simplicity, the type is currently just a Node * identifier in the form of a string and not a Node reference. * This might be changed in the future. * * @param string $type The type. * * @return self * * @throws \InvalidArgumentException If the type is not a string. No * further checks are currently done. */ public function setType($type) { if (!is_string($type)) { throw new \InvalidArgumentException('type must be a string.'); } $this->type = $type; return $this; } /** * Get the type * * For the sake of simplicity, the type is currently just a Node * identifier in the form of a string and not a Node reference. * This might be changed in the future. * * @return string The type. */ public function getType() { return $this->type; } /** * {@inheritdoc} */ public function toJsonLd($useNativeTypes = true) { $result = new JsonObject(); if (RdfConstants::XSD_STRING === $this->type) { $result->{'@value'} = $this->value; return $result; } if (true === $useNativeTypes) { if (RdfConstants::XSD_BOOLEAN === $this->type) { if ('true' === $this->value) { $result->{'@value'} = true; return $result; } elseif ('false' === $this->value) { $result->{'@value'} = false; return $result; } } elseif (RdfConstants::XSD_INTEGER === $this->type) { if (preg_match('/^[\+|-]?\d+$/', trim($this->value))) { $result->{'@value'} = intval($this->value); return $result; } } elseif (RdfConstants::XSD_DOUBLE === $this->type) { // TODO Need to handle +/-INF and NaN as well? if (preg_match('/^[\+|-]?\d+(?:\.\d*)?(?:[eE][\+|-]?\d+)?$/', trim($this->value))) { $result->{'@value'} = floatval($this->value); return $result; } // TODO Need to preserve doubles without fraction for round-tripping?? } } $result->{'@value'} = $this->value; $result->{'@type'} = $this->type; return $result; } /** * {@inheritdoc} */ public function equals($other) { if (get_class($this) !== get_class($other)) { return false; } return ($this->value === $other->value) && ($this->type === $other->type); } } JsonLD-1.2.1/Value.php000066400000000000000000000066021431525543500144440ustar00rootroot00000000000000 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace ML\JsonLD; use stdClass as JsonObject; /** * Value is the abstract base class used for typed values and * language-tagged strings. * * @author Markus Lanthaler */ abstract class Value implements JsonLdSerializable { /** * The value in the form of a string * * @var string */ protected $value; /** * Set the value * * @param string $value The value. * * @return self * * @throws \InvalidArgumentException If the value is not a string. */ public function setValue($value) { if (!is_string($value)) { throw new \InvalidArgumentException('value must be a string.'); } $this->value = $value; return $this; } /** * Get the value * * @return string The value. */ public function getValue() { return $this->value; } /** * Create a LanguageTaggedString or TypedValue from a JSON-LD element * * If the passed value element can't be transformed to a language-tagged * string or a typed value false is returned. * * @param JsonObject $element The JSON-LD element * * @return false|LanguageTaggedString|TypedValue The parsed object */ public static function fromJsonLd(JsonObject $element) { if (false === property_exists($element, '@value')) { return false; } $value = $element->{'@value'}; $type = (property_exists($element, '@type')) ? $element->{'@type'} : null; $language = (property_exists($element, '@language')) ? $element->{'@language'} : null; if (is_int($value) || is_float($value)) { if (($value != (int) $value) || (RdfConstants::XSD_DOUBLE === $type)) { $value = preg_replace('/(0{0,14})E(\+?)/', 'E', sprintf('%1.15E', $value)); if ((null === $type) && (null === $language)) { return new TypedValue($value, RdfConstants::XSD_DOUBLE); } } else { $value = sprintf('%d', $value); if ((null === $type) && (null === $language)) { return new TypedValue($value, RdfConstants::XSD_INTEGER); } } } elseif (is_bool($value)) { $value = ($value) ? 'true' : 'false'; if ((null === $type) && (null === $language)) { return new TypedValue($value, RdfConstants::XSD_BOOLEAN); } } elseif (false === is_string($value)) { return false; } // @type gets precedence if ((null === $type) && (null !== $language)) { return new LanguageTaggedString($value, $language); } return new TypedValue($value, (null === $type) ? RdfConstants::XSD_STRING : $type); } /** * Compares this instance to the specified value. * * @param mixed $other The value this instance should be compared to. * * @return bool Returns true if the passed value is the same as this * instance; false otherwise. */ abstract public function equals($other); } JsonLD-1.2.1/composer.json000066400000000000000000000013161431525543500153760ustar00rootroot00000000000000{ "name": "ml/json-ld", "type": "library", "description": "JSON-LD Processor for PHP", "keywords": [ "JSON-LD", "jsonld" ], "homepage": "http://www.markus-lanthaler.com", "license": "MIT", "authors": [ { "name": "Markus Lanthaler", "email": "mail@markus-lanthaler.com", "homepage": "http://www.markus-lanthaler.com", "role": "Developer" } ], "require": { "php": ">=5.3.0", "ext-json": "*", "ml/iri": "^1.1.1" }, "require-dev": { "json-ld/tests": "1.0", "phpunit/phpunit": "^4" }, "autoload": { "psr-4": { "ML\\JsonLD\\": "" } } } JsonLD-1.2.1/phpunit.xml.dist000066400000000000000000000046721431525543500160370ustar00rootroot00000000000000 ./Test/ ./ ./Test ./vendor