Imported code from old repository
This commit is contained in:
464
CloudObjects/SDK/ObjectRetriever.php
Normal file
464
CloudObjects/SDK/ObjectRetriever.php
Normal file
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
namespace CloudObjects\SDK;
|
||||
|
||||
use Exception;
|
||||
use ML\IRI\IRI, ML\JsonLD\JsonLD;
|
||||
use Doctrine\Common\Cache\RedisCache;
|
||||
use Psr\Log\LoggerInterface, Psr\Log\LoggerAwareTrait;
|
||||
use GuzzleHttp\ClientInterface, GuzzleHttp\Client, GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Kevinrob\GuzzleCache\CacheMiddleware, Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
|
||||
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
|
||||
use CloudObjects\SDK\Exceptions\CoreAPIException;
|
||||
use CloudObjects\SDK\AccountGateway\AccountContext;
|
||||
|
||||
/**
|
||||
* The ObjectRetriever provides access to objects on CloudObjects.
|
||||
*/
|
||||
class ObjectRetriever {
|
||||
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $client;
|
||||
private $prefix;
|
||||
private $options;
|
||||
private $cache;
|
||||
private $objects;
|
||||
|
||||
const CO_API_URL = 'https://api.cloudobjects.net/';
|
||||
|
||||
const REVISION_PROPERTY = 'coid://cloudobjects.io/isAtRevision';
|
||||
|
||||
public function __construct($options = []) {
|
||||
// Merge options with defaults
|
||||
$this->options = array_merge([
|
||||
'cache_provider' => 'none',
|
||||
'cache_prefix' => 'clobj:',
|
||||
'cache_ttl' => 60,
|
||||
'static_config_path' => null,
|
||||
'auth_ns' => null,
|
||||
'auth_secret' => null,
|
||||
'api_base_url' => null,
|
||||
'logger' => null,
|
||||
'timeout' => 20,
|
||||
'connect_timeout' => 5
|
||||
], $options);
|
||||
|
||||
// Set up object cache
|
||||
switch ($this->options['cache_provider']) {
|
||||
case 'none':
|
||||
// no caching
|
||||
$this->cache = null;
|
||||
break;
|
||||
case 'redis':
|
||||
// caching with Redis
|
||||
$redis = new \Redis();
|
||||
$redis->pconnect(
|
||||
isset($this->options['cache_provider.redis.host']) ? $this->options['cache_provider.redis.host'] : '127.0.0.1',
|
||||
isset($this->options['cache_provider.redis.port']) ? $this->options['cache_provider.redis.port'] : 6379);
|
||||
|
||||
$this->cache = new RedisCache();
|
||||
$this->cache->setRedis($redis);
|
||||
break;
|
||||
case 'file':
|
||||
// caching on the filesystem
|
||||
$this->cache = new \Doctrine\Common\Cache\FilesystemCache(
|
||||
isset($this->options['cache_provider.file.directory']) ? $this->options['cache_provider.file.directory'] : sys_get_temp_dir()
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Valid values for cache_provider are: none, redis, file');
|
||||
}
|
||||
|
||||
// Set up logger
|
||||
if (is_a($this->options['logger'], LoggerInterface::class))
|
||||
$this->setLogger($this->options['logger']);
|
||||
|
||||
// Set up handler stack
|
||||
$stack = HandlerStack::create();
|
||||
|
||||
// Add HTTP cache if specified
|
||||
if (isset($this->cache)) {
|
||||
$stack->push(
|
||||
new CacheMiddleware(
|
||||
new PrivateCacheStrategy(
|
||||
new DoctrineCacheStorage($this->cache)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize client
|
||||
$options = [
|
||||
'base_uri' => isset($options['api_base_url']) ? $options['api_base_url'] : self::CO_API_URL,
|
||||
'handler' => $stack,
|
||||
'connect_timeout' => $this->options['connect_timeout'],
|
||||
'timeout' => $this->options['timeout']
|
||||
];
|
||||
|
||||
if (isset($this->options['auth_ns']) && isset($this->options['auth_secret']))
|
||||
$options['auth'] = [$this->options['auth_ns'], $this->options['auth_secret']];
|
||||
|
||||
$this->client = new Client($options);
|
||||
}
|
||||
|
||||
private function logInfoWithTime($message, $ts) {
|
||||
if (isset($this->logger))
|
||||
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]);
|
||||
}
|
||||
|
||||
private function getFromCache($id) {
|
||||
return (isset($this->cache) && $this->cache->contains($this->options['cache_prefix'].$id))
|
||||
? $this->cache->fetch($this->options['cache_prefix'].$id) : null;
|
||||
}
|
||||
|
||||
private function putIntoCache($id, $data, $ttl) {
|
||||
if (isset($this->cache))
|
||||
$this->cache->save($this->options['cache_prefix'].$id, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP client that is used to access the API.
|
||||
*/
|
||||
public function getClient() {
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the HTTP client that is used to access the API.
|
||||
*
|
||||
* @param ClientInterface $client The HTTP client.
|
||||
* @param string $prefix An optional prefix (e.g. an AccountGateway mountpoint)
|
||||
*/
|
||||
public function setClient(ClientInterface $client, $prefix = null) {
|
||||
$this->client = $client;
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of this object retriever that uses the given account
|
||||
* context to access the API as a developer API. Cache settings are inherited
|
||||
* but the prefix is extended to keep cache content specific to account.
|
||||
*
|
||||
* @param AccountContext $accountContext
|
||||
* @param string $mountpointName The name for the API mountpoint.
|
||||
*/
|
||||
public function withAccountContext(AccountContext $accountContext, string $mountpointName) {
|
||||
$newRetriever = new self($this->options);
|
||||
$newRetriever->options['cache_prefix'] .= (string)$accountContext->getAAUID();
|
||||
$newRetriever->client = $accountContext->getClient();
|
||||
$newRetriever->prefix = '/'.$mountpointName.'/';
|
||||
|
||||
return $newRetriever;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object description from CloudObjects. Attempts to get object
|
||||
* from in-memory cache first, stored static configurations next,
|
||||
* configured external cache third, and finally calls the Object API
|
||||
* on CloudObjects Core. Returns null if the object was not found.
|
||||
*
|
||||
* @param IRI $coid COID of the object
|
||||
* @return Node|null
|
||||
*/
|
||||
public function getObject(IRI $coid) {
|
||||
if (!COIDParser::isValidCOID($coid))
|
||||
throw new Exception("Not a valid COID.");
|
||||
|
||||
$uriString = (string)$coid;
|
||||
|
||||
if (isset($this->objects[$uriString]))
|
||||
// Return from in-memory cache if it exists
|
||||
return $this->objects[$uriString];
|
||||
|
||||
$ts = microtime(true);
|
||||
|
||||
if (isset($this->options['static_config_path'])) {
|
||||
$location = realpath($this->options['static_config_path'].DIRECTORY_SEPARATOR.
|
||||
$coid->getHost().str_replace('/', DIRECTORY_SEPARATOR, $coid->getPath())
|
||||
.DIRECTORY_SEPARATOR.'object.jsonld');
|
||||
|
||||
if ($location && file_exists($location)) {
|
||||
$object = $location;
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from static configuration.', $ts);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($object)) {
|
||||
$object = $this->getFromCache($uriString);
|
||||
if (isset($object))
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from object cache.', $ts);
|
||||
}
|
||||
|
||||
if (!isset($object)) {
|
||||
try {
|
||||
$response = $this->client
|
||||
->get((isset($this->prefix) ? $this->prefix : '').$coid->getHost().$coid->getPath().'/object',
|
||||
['headers' => ['Accept' => 'application/ld+json']]);
|
||||
|
||||
$object = (string)$response->getBody();
|
||||
$this->putIntoCache($uriString, $object, $this->options['cache_ttl']);
|
||||
$this->logInfoWithTime('Fetched <'.$uriString.'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
} catch (RequestException $e) {
|
||||
if ($e->hasResponse())
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> not found in Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
|
||||
else
|
||||
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API.', $ts);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$document = JsonLD::getDocument($object);
|
||||
$this->objects[$uriString] = $document->getGraph()->getNode($uriString);
|
||||
return $this->objects[$uriString];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all object descriptions for objects in a specific namespace
|
||||
* and with a certain type from CloudObjects. Adds individual objects
|
||||
* to cache and returns a list of COIDs (as IRI) for them. The list
|
||||
* itself is not cached, which means that every call of this function
|
||||
* goes to the Object API.
|
||||
*
|
||||
* @param IRI $namespaceCoid COID of the namespace
|
||||
* @param $type RDF type that objects should have
|
||||
* @return array<IRI>
|
||||
*/
|
||||
public function fetchObjectsInNamespaceWithType(IRI $namespaceCoid, $type) {
|
||||
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
|
||||
throw new Exception("Not a valid namespace COID.");
|
||||
|
||||
$ts = microtime(true);
|
||||
$type = (string)$type;
|
||||
|
||||
try {
|
||||
$response = $this->client
|
||||
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/all',
|
||||
[
|
||||
'headers' => [ 'Accept' => 'application/ld+json' ],
|
||||
'query' => [ 'type' => $type ]
|
||||
]);
|
||||
|
||||
$document = JsonLD::getDocument((string)$response->getBody());
|
||||
$allObjects = $document->getGraph()->getNodesByType($type);
|
||||
$allIris = [];
|
||||
foreach ($allObjects as $object) {
|
||||
$iri = new IRI($object->getId());
|
||||
if (!COIDParser::isValidCOID($iri)) continue;
|
||||
if ($iri->getHost() != $namespaceCoid->getHost()) continue;
|
||||
|
||||
$this->objects[$object->getId()] = $object;
|
||||
$this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']);
|
||||
$allIris[] = $iri;
|
||||
}
|
||||
|
||||
$this->logInfoWithTime('Fetched all objects with <'.$type.'> for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
} catch (Exception $e) {
|
||||
throw new CoreAPIException;
|
||||
}
|
||||
|
||||
return $allIris;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all object descriptions for objects in a specific namespace
|
||||
* from CloudObjects. Adds individual objects to cache and returns a
|
||||
* list of COIDs (as IRI) for them. The list itself is not cached,
|
||||
* which means that every call of this function goes to the Object API.
|
||||
*
|
||||
* @param IRI $namespaceCoid COID of the namespace
|
||||
* @return array<IRI>
|
||||
*/
|
||||
public function fetchAllObjectsInNamespace(IRI $namespaceCoid) {
|
||||
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
|
||||
throw new Exception("Not a valid namespace COID.");
|
||||
|
||||
$ts = microtime(true);
|
||||
|
||||
try {
|
||||
$response = $this->client
|
||||
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/all',
|
||||
[ 'headers' => [ 'Accept' => 'application/ld+json' ] ]);
|
||||
|
||||
$document = JsonLD::getDocument((string)$response->getBody());
|
||||
$allObjects = $document->getGraph()->getNodes();
|
||||
$allIris = [];
|
||||
foreach ($allObjects as $object) {
|
||||
$iri = new IRI($object->getId());
|
||||
if (!COIDParser::isValidCOID($iri)) continue;
|
||||
if ($iri->getHost() != $namespaceCoid->getHost()) continue;
|
||||
|
||||
$this->objects[$object->getId()] = $object;
|
||||
$this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']);
|
||||
$allIris[] = $iri;
|
||||
}
|
||||
|
||||
$this->logInfoWithTime('Fetched all objects for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new CoreAPIException;
|
||||
}
|
||||
|
||||
return $allIris;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of COIDs for all objects in a specific namespace
|
||||
* from CloudObjects, but not the objects itself. The list is not cached,
|
||||
* which means that every call of this function goes to the Object API.
|
||||
*
|
||||
* @param IRI $namespaceCoid COID of the namespace
|
||||
* @return array<IRI>
|
||||
*/
|
||||
public function getCOIDListForNamespace(IRI $namespaceCoid) {
|
||||
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
|
||||
throw new Exception("Not a valid namespace COID.");
|
||||
|
||||
$ts = microtime(true);
|
||||
|
||||
try {
|
||||
$response = $this->client
|
||||
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/coids',
|
||||
[ 'headers' => [ 'Accept' => 'application/ld+json' ] ]);
|
||||
|
||||
$document = JsonLD::getDocument((string)$response->getBody());
|
||||
$containerNode = $document->getGraph()->getNode('co-namespace-members://'.$namespaceCoid->getHost());
|
||||
|
||||
$reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]);
|
||||
$allIris = $reader->getAllValuesIRI($containerNode, 'rdfs:member');
|
||||
|
||||
$this->logInfoWithTime('Fetched object list for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new CoreAPIException;
|
||||
}
|
||||
|
||||
return $allIris;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of COIDs for all objects in a specific namespace
|
||||
* from CloudObjects, but not the objects itself. The list is not cached,
|
||||
* which means that every call of this function goes to the Object API.
|
||||
*
|
||||
* @param IRI $namespaceCoid COID of the namespace
|
||||
* @param $type RDF type that objects should have
|
||||
* @return array<IRI>
|
||||
*/
|
||||
public function getCOIDListForNamespaceWithType(IRI $namespaceCoid, $type) {
|
||||
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
|
||||
throw new Exception("Not a valid namespace COID.");
|
||||
|
||||
$ts = microtime(true);
|
||||
$type = (string)$type;
|
||||
|
||||
try {
|
||||
$response = $this->client
|
||||
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/coids',
|
||||
[
|
||||
'headers' => [ 'Accept' => 'application/ld+json' ],
|
||||
'query' => [ 'type' => $type ]
|
||||
]);
|
||||
|
||||
$document = JsonLD::getDocument((string)$response->getBody());
|
||||
$containerNode = $document->getGraph()->getNode('co-namespace-members://'.$namespaceCoid->getHost());
|
||||
|
||||
$reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]);
|
||||
$allIris = $reader->getAllValuesIRI($containerNode, 'rdfs:member');
|
||||
|
||||
$this->logInfoWithTime('Fetched object list with <'.$type.'> for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new CoreAPIException;
|
||||
}
|
||||
|
||||
return $allIris;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object description from CloudObjects. Shorthand method for
|
||||
* "getObject" which allows passing the COID as string instead of IRI.
|
||||
*
|
||||
* @param any $coid
|
||||
* @return Node|null
|
||||
*/
|
||||
public function get($coid) {
|
||||
if (is_string($coid))
|
||||
return $this->getObject(new IRI($coid));
|
||||
|
||||
if (is_object($coid) && get_class($coid)=='ML\IRI\IRI')
|
||||
return $this->getObject($coid);
|
||||
|
||||
throw new Exception('COID must be passed as a string or an IRI object.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a object's attachment.
|
||||
*
|
||||
* @param IRI $coid
|
||||
* @param string $filename
|
||||
*/
|
||||
public function getAttachment(IRI $coid, $filename) {
|
||||
$object = $this->getObject($coid);
|
||||
|
||||
if (!$object)
|
||||
// Cannot get attachment for non-existing object
|
||||
return null;
|
||||
|
||||
$ts = microtime(true);
|
||||
|
||||
$cacheId = $object->getId().'#'.$filename;
|
||||
$fileData = $this->getFromCache($cacheId);
|
||||
|
||||
// Parse cached data into revision and content
|
||||
if (isset($fileData)) {
|
||||
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from object cache.', $ts);
|
||||
list($fileRevision, $fileContent) = explode('#', $fileData, 2);
|
||||
}
|
||||
|
||||
if (!isset($fileData)
|
||||
|| $fileRevision!=$object->getProperty(self::REVISION_PROPERTY)->getValue()) {
|
||||
|
||||
// Does not exist in cache or is outdated, fetch from CloudObjects
|
||||
try {
|
||||
$response = $this->client->get((isset($this->prefix) ? $this->prefix : '').$coid->getHost().$coid->getPath()
|
||||
.'/'.basename($filename));
|
||||
|
||||
$fileContent = $response->getBody()->getContents();
|
||||
$fileData = $object->getProperty(self::REVISION_PROPERTY)->getValue().'#'.$fileContent;
|
||||
$this->putIntoCache($cacheId, $fileData, 0);
|
||||
|
||||
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from Core API ['.$response->getStatusCode().'].', $ts);
|
||||
} catch (Exception $e) {
|
||||
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> not found in Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
|
||||
// ignore exception - treat as non-existing file
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $fileContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the object that describes the namespace provided with the "auth_ns"
|
||||
* configuration option.
|
||||
*
|
||||
* @return Node
|
||||
*/
|
||||
public function getAuthenticatingNamespaceObject() {
|
||||
if (!isset($this->options['auth_ns']))
|
||||
throw new Exception("Missing 'auth_ns' configuration option.");
|
||||
|
||||
$namespaceCoid = COIDParser::fromString($this->options['auth_ns']);
|
||||
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
|
||||
throw new Exception("The 'auth_ns' configuration option is not a valid namespace/root COID.");
|
||||
|
||||
return $this->getObject($namespaceCoid);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user