diff --git a/.gitignore b/.gitignore index 826af1be1..5f92d46db 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ install/downloads site/ services/ResourceService/composer.lock + +services/ResourceServiceProvider/composer.lock diff --git a/services/CollectionService/.gitignore b/services/CollectionService/.gitignore new file mode 100644 index 000000000..48b8bf907 --- /dev/null +++ b/services/CollectionService/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/services/CollectionService/composer.json b/services/CollectionService/composer.json new file mode 100644 index 000000000..c2503ea9b --- /dev/null +++ b/services/CollectionService/composer.json @@ -0,0 +1,32 @@ +{ + "name": "islandora/collection-service", + "description": "RESTful service providing resources in Fedora 4", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Islandora-CLAW/chullo" + + }, + { + "type": "path", + "url": "/Users/dpino/Desktop/Development/ISLANDORAWORK/CLAW_MICRO/islandora/services/ResourceServiceProvider" + } + ], + "require": { + "islandora/chullo": "dev-master", + "islandora/resource-service" : "dev-sprint-002", + "silex/silex": "^1.3", + "symfony/config": "^3.0", + "twig/twig": "^1.23", + "symfony/yaml": "^3.0" + }, + "autoload": { + "psr-4": {"Islandora\\CollectionService\\": "src/"} + }, + "authors": [ + { + "name": "Diego Pino Navarro", + "email": "dpino@krayon.cl" + } + ] +} diff --git a/services/CollectionService/config/settings.dev.yml b/services/CollectionService/config/settings.dev.yml new file mode 100644 index 000000000..4c88cff65 --- /dev/null +++ b/services/CollectionService/config/settings.dev.yml @@ -0,0 +1,9 @@ +# Islandora Dev Settings to be used with $app['debug'] == TRUE +islandora: + fedoraProtocol: http + fedoraHost: "localhost:8080" + fedoraPath: /rest + tripleProtocol: http + tripleHost: "localhost:9999" + triplePath: /bigdata/sparql + resourceIdRegex: "(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})?" \ No newline at end of file diff --git a/services/CollectionService/config/settings.yml b/services/CollectionService/config/settings.yml new file mode 100644 index 000000000..00a4370f8 --- /dev/null +++ b/services/CollectionService/config/settings.yml @@ -0,0 +1,8 @@ +islandora: + fedoraProtocol: http + fedoraHost: "localhost:8080" + fedoraPath: /fcrepo/rest + tripleProtocol: http + tripleHost: "localhost:8080" + triplePath: /bigdata/sparql + resourceIdRegex: "(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})?" \ No newline at end of file diff --git a/services/CollectionService/src/index.php b/services/CollectionService/src/index.php new file mode 100644 index 000000000..2ac74cb18 --- /dev/null +++ b/services/CollectionService/src/index.php @@ -0,0 +1,106 @@ +register(new \Silex\Provider\ServiceControllerServiceProvider()); +$app->register(new \Silex\Provider\TwigServiceProvider(), array( + 'twig.path' => __DIR__.'/../templates', +)); + +$app['twig'] = $app->share($app->extend('twig', function($twig, $app) { + return $twig; +})); +$islandoraResourceServiceProvider = new \Islandora\ResourceService\Provider\ResourceServiceProvider; + +$app->register($islandoraResourceServiceProvider); +$app->mount("/islandora", $islandoraResourceServiceProvider); +$app->register(new \Islandora\ResourceService\Provider\UuidServiceProvider(), array( + 'UuidServiceProvider.default_namespace' => $app['config']['islandora']['defaultNamespaceDomainUuuidV5'], +)); + +//$app['uuid'] = $app['islandora.uuid5'](rand()); +$app['uuid'] = $app['islandora.uuid4']; + +/** + * Still Not used, this function will check for content type +*/ +$isFedora4Content = function (Request $request) use ($app) { + $rdf_content_types = array( + "text/turtle", + "text/rdf+n3", + "application/n3", + "text/n3", + "application/rdf+xml", + "application/n-triples", + "application/sparql-update" + ); + if (in_array($request->headers->get('Content-type'), $rdf_content_types)) { + return true; + } + return false; +}; + +/** + * Convert returned Guzzle responses to Symfony responses. + */ + +$app->view(function (ResponseInterface $psr7) { + return new Response($psr7->getBody(), $psr7->getStatusCode(), $psr7->getHeaders()); +}); + +/** + * Collection POST route test. Does nothing more than redirect from collection to mounted + * takes 'rx' and/or 'checksum' as optional query arguments + */ +$app->post("/islandora/collection",function (Application $app, Request $request) { + error_log($app['uuid']); + $tx = $request->query->get('tx', ""); + $url = $request->getUriForPath('/islandora/resource/'); + error_log($url); + //static public Request create(string $uri, string $method = 'GET', array $parameters = array(), array $cookies = array(), array $files = array(), array $server = array(), string $content = null) + $subRequest = Request::create($url, 'GET', array(), $request->cookies->all(), $request->files->all(), $request->server->all()); + error_log($subRequest->__toString()); + $response = $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false); + return $response; +}); + +$app->error(function (\Symfony\Component\HttpKernel\Exception\HttpException $e, $code) use ($app){ + if ($app['debug']) { + return; + } + return new response(sprintf('Islandora Resource Service exception: %s / HTTP %d response', $e->getMessage(), $code), $code); +}); +$app->error(function (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, $code) use ($app){ + if ($app['debug']) { + return; + } + //Not sure what the best "verbose" message is + return new response(sprintf('Islandora Resource Service exception: %s / HTTP %d response', $e->getMessage(), $code), $code); +}); +$app->error(function (\Exception $e, $code) use ($app){ + if ($app['debug']) { + return; + } + return new response(sprintf('Islandora Resource Service uncatched exception: %s %d response', $e->getMessage(), $code), $code); +}); + + +$app->run(); diff --git a/services/CollectionService/templates/createIndirectContainerfromTS.sparql b/services/CollectionService/templates/createIndirectContainerfromTS.sparql new file mode 100644 index 000000000..b8fe9a70e --- /dev/null +++ b/services/CollectionService/templates/createIndirectContainerfromTS.sparql @@ -0,0 +1,5 @@ +PREFIX nfo: +PREFIX rdf: +SELECT ?s WHERE { + ?s nfo:uuid "{{uuid}}"^^ . +} diff --git a/services/CollectionService/templates/createPCDMCollectionfromTS.sparql b/services/CollectionService/templates/createPCDMCollectionfromTS.sparql new file mode 100644 index 000000000..b8fe9a70e --- /dev/null +++ b/services/CollectionService/templates/createPCDMCollectionfromTS.sparql @@ -0,0 +1,5 @@ +PREFIX nfo: +PREFIX rdf: +SELECT ?s WHERE { + ?s nfo:uuid "{{uuid}}"^^ . +} diff --git a/services/CollectionService/templates/getResourceByUUIDfromTS.sparql b/services/CollectionService/templates/getResourceByUUIDfromTS.sparql new file mode 100644 index 000000000..b8fe9a70e --- /dev/null +++ b/services/CollectionService/templates/getResourceByUUIDfromTS.sparql @@ -0,0 +1,5 @@ +PREFIX nfo: +PREFIX rdf: +SELECT ?s WHERE { + ?s nfo:uuid "{{uuid}}"^^ . +} diff --git a/services/ResourceServiceProvider/.gitignore b/services/ResourceServiceProvider/.gitignore new file mode 100644 index 000000000..48b8bf907 --- /dev/null +++ b/services/ResourceServiceProvider/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/services/ResourceServiceProvider/composer.json b/services/ResourceServiceProvider/composer.json new file mode 100644 index 000000000..a79b8e6d9 --- /dev/null +++ b/services/ResourceServiceProvider/composer.json @@ -0,0 +1,27 @@ +{ + "name": "islandora/resource-service", + "description": "RESTful service providing resources in Fedora 4", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/Islandora-CLAW/chullo" + } + ], + "require": { + "islandora/chullo": "dev-master", + "silex/silex": "^1.3", + "symfony/config": "^3.0", + "twig/twig": "^1.23", + "symfony/yaml": "^3.0", + "ramsey/uuid": "^3.1" + }, + "autoload": { + "psr-4": {"Islandora\\ResourceService\\": "src/"} + }, + "authors": [ + { + "name": "Diego Pino Navarro", + "email": "dpino@krayon.cl" + } + ] +} diff --git a/services/ResourceServiceProvider/config/settings.dev.yml b/services/ResourceServiceProvider/config/settings.dev.yml new file mode 100644 index 000000000..5f0ca8418 --- /dev/null +++ b/services/ResourceServiceProvider/config/settings.dev.yml @@ -0,0 +1,11 @@ +# Islandora Dev Settings to be used with $app['debug'] == TRUE +islandora: + fedoraProtocol: http + fedoraHost: "localhost:8080" + fedoraPath: /rest + tripleProtocol: http + tripleHost: "localhost:9999" + triplePath: /bigdata/sparql + resourceIdRegex: "(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})?" +# This domain is used as namespace (hashed) when generating UUID V5 identifiers + defaultNamespaceDomainUuuidV5: "www.islandora.ca" \ No newline at end of file diff --git a/services/ResourceServiceProvider/config/settings.yml b/services/ResourceServiceProvider/config/settings.yml new file mode 100644 index 000000000..579f83fbe --- /dev/null +++ b/services/ResourceServiceProvider/config/settings.yml @@ -0,0 +1,10 @@ +islandora: + fedoraProtocol: http + fedoraHost: "localhost:8080" + fedoraPath: /fcrepo/rest + tripleProtocol: http + tripleHost: "localhost:8080" + triplePath: /bigdata/sparql + resourceIdRegex: "(?:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})?" + # This domain is used as namespace (hashed) when generating UUID V5 identifiers, replace with your own + defaultNamespaceDomainUuuidV5: "www.islandora.ca" \ No newline at end of file diff --git a/services/ResourceServiceProvider/src/Controller/ResourceController.php b/services/ResourceServiceProvider/src/Controller/ResourceController.php new file mode 100644 index 000000000..f28db26d2 --- /dev/null +++ b/services/ResourceServiceProvider/src/Controller/ResourceController.php @@ -0,0 +1,84 @@ +query->get('tx', ""); + $metadata = $request->query->get('metadata', FALSE) ? '/fcr:metadata' : ""; + try { + $response = $app['api']->getResource($app->escape($id).'/'.$child.$metadata, $request->headers->all(), $tx); + } + catch (\Exception $e) { + $app->abort(503, 'Chullo says "Fedora4 Repository Not available"'); + } + return $response; + } + /** + * Resource POST route controller. takes $id (valid UUID or empty) for the parent resource as first value to match + * takes 'rx' and/or 'checksum' as optional query arguments + * @see https://wiki.duraspace.org/display/FEDORA40/RESTful+HTTP+API#RESTfulHTTPAPI-BluePOSTCreatenewresourceswithinaLDPcontainer + */ + public function post(Application $app, Request $request, $id) { + $tx = $request->query->get('tx', ""); + $checksum = $request->query->get('checksum', ""); + try { + $response = $app['api']->createResource($app->escape($id), $request->getContent(), $request->headers->all(), $tx, $checksum); + } + catch (\Exception $e) { + $app->abort(503, '"Chullo says Fedora4 Repository is Not available"'); + } + return $response; + } + /** + * Resource PUT route. takes $id (valid UUID or empty) for the resource to be update/created as first value to match, + * optional a Child resource relative path + * takes 'rx' and/or 'checksum' as optional query arguments + * @see https://wiki.duraspace.org/display/FEDORA40/RESTful+HTTP+API#RESTfulHTTPAPI-YellowPUTCreatearesourcewithaspecifiedpath,orreplacethetriplesassociatedwitharesourcewiththetriplesprovidedintherequestbody. + */ + public function put(Application $app, Request $request, $id, $child) { + $tx = $request->query->get('tx', ""); + $checksum = $request->query->get('checksum', ""); + try { + $response = $app['api']->saveResource($app->escape($id).'/'.$child, $request->getContent(), $request->headers->all(), $tx, $checksum); + } + catch (\Exception $e) { + $app->abort(503, '"Chullo says Fedora4 Repository is Not available"'); + } + return $response; + } + public function patch(Application $app, Request $request, $id, $child) { + $tx = $request->query->get('tx', ""); + try { + $response = $app['api']->modifyResource($app->escape($id).'/'.$child, $request->getContent(), $request->headers->all(), $tx); + } + catch (\Exception $e) { + $app->abort(503, '"Chullo says Fedora4 Repository is Not available"'); + } + return $response; + } + public function delete(Application $app, Request $request, $id, $child) { + $tx = $request->query->get('tx', ""); + $force = $request->query->get('force', FALSE); + try { + $response = $app['api']->deleteResource($app->escape($id).'/'.$child, $tx); + //remove tombstone also if 'force' == true and previous response is 204 + if ((204 == $response->getStatusCode() || 410 == $response->getStatusCode()) && $force) { + $response= $app['api']->deleteResource($app->escape($id).'/'.$child.'/fcr:tombstone', $tx); + } + } + catch (\Exception $e) { + $app->abort(503, '"Chullo says Fedora4 Repository is Not available"'); + } + return $response; + } + +} \ No newline at end of file diff --git a/services/ResourceServiceProvider/src/Provider/ResourceServiceProvider.php b/services/ResourceServiceProvider/src/Provider/ResourceServiceProvider.php new file mode 100644 index 000000000..92e6e6d84 --- /dev/null +++ b/services/ResourceServiceProvider/src/Provider/ResourceServiceProvider.php @@ -0,0 +1,166 @@ +share(function() use ($app) { + return new \Islandora\ResourceService\Controller\ResourceController($app); + }); + if (!isset($app['twig'])) { + $app['twig'] = $app->share($app->extend('twig', function($twig, $app) { + return $twig; + })); + } + if (!isset($app['api'])) { + $app['api'] = $app->share(function() use ($app) { + return FedoraApi::create($app['config']['islandora']['fedoraProtocol'].'://'.$app['config']['islandora']['fedoraHost'].$app['config']['islandora']['fedoraPath']); + }); + } + if (!isset($app['triplestore'])) { + $app['triplestore'] = $app->share(function() use ($app) { + return TriplestoreClient::create($app['config']['islandora']['tripleProtocol'].'://'.$app['config']['islandora']['tripleHost'].$app['config']['islandora']['triplePath']); + }); + } + /** + * Ultra simplistic YAML settings loader. + */ + if (!isset($app['config'])) { + $app['config'] = $app->share(function() use ($app){ + if ($app['debug']) { + $configFile = __DIR__.'/../../config/settings.dev.yml'; + } + else { + $configFile = __DIR__.'/../../config/settings.yml'; + } + $settings = Yaml::parse(file_get_contents($configFile)); + return $settings; + }); + } + /** + * Make our middleware callback functions protected + */ + /** + * before middleware to handle browser requests. + */ + $app['islandora.htmlHeaderToTurtle'] = $app->protect(function(Request $request) { + // In case the request was made by a browser, avoid + // returning the whole Fedora4 API Rest interface page. + if (0 === strpos($request->headers->get('Accept'),'text/html')) { + $request->headers->set('Accept', 'text/turtle', TRUE); + } + }); + + + /** + * before middleware to normalize host header to same as fedora's running + * instance. + */ + $app['islandora.hostHeaderNormalize'] = $app->protect(function(Request $request) use ($app) { + // Normalize Host header to Repo's real location + $request->headers->set('Host', $app['config']['islandora']['fedoraHost'], TRUE); + }); + + /** + * Converts request $id (uuid) into a fedora4 resourcePath + */ + $app['islandora.idToUri'] = $app->protect(function ($id) use ($app) { + // Run only if $id given /can also be refering root resource, + // we accept only UUID V4 or empty + if (NULL != $id) { + $sparql_query = $app['twig']->render('getResourceByUUIDfromTS.sparql', array( + 'uuid' => $id, + )); + try { + $sparql_result = $app['triplestore']->query($sparql_query); + } + catch (\Exception $e) { + $app->abort(503, 'Chullo says "Triple Store Not available"'); + } + // We only assign one in case of multiple ones + // Will have to check for edge cases? + foreach ($sparql_result as $triple) { + return $triple->s->getUri(); + } + // Abort the routes if we don't get a subject from the tripple. + $app->abort(404, sprintf('Failed getting resource Path for "%s" from triple store', $id)); + } + else { + // If $id is empty then assume we are dealing with fedora base rest endpoint + return $app['config']['islandora']['fedoraProtocol'].'://'.$app['config']['islandora']['fedoraHost'].$app['config']['islandora']['fedoraPath']; + } + }); + + } + + function boot(Application $app) { + } + + /** + * Part of ControllerProviderInterface + */ + public function connect(Application $app) { + $controllers = $app['controllers_factory']; + // + // Define routing referring to controller services + // + $controllers->get("/resource/{id}/{child}", "islandora.resourcecontroller:get") + ->convert('id', $app['islandora.idToUri']) + ->assert('id',$app['config']['islandora']['resourceIdRegex']) + ->before($app['islandora.hostHeaderNormalize']) + ->before($app['islandora.htmlHeaderToTurtle']) + ->value('id',"") + ->value('child',"") + ->bind('islandora.resourceGet'); + $controllers->post("/resource/{id}", "islandora.resourcecontroller:post") + ->convert('id', $app['islandora.idToUri']) + ->assert('id',$app['config']['islandora']['resourceIdRegex']) + ->before($app['islandora.hostHeaderNormalize']) + ->before($app['islandora.htmlHeaderToTurtle']) + ->value('id',"") + ->bind('islandora.resourcePost'); + $controllers->put("/resource/{id}/{child}", "islandora.resourcecontroller:put") + ->convert('id', $app['islandora.idToUri']) + ->assert('id',$app['config']['islandora']['resourceIdRegex']) + ->before($app['islandora.hostHeaderNormalize']) + ->before($app['islandora.htmlHeaderToTurtle']) + ->value('id',"") + ->value('child',"") + ->bind('islandora.resourcePut'); + $controllers->patch("/resource/{id}/{child}", "islandora.resourcecontroller:patch") + ->convert('id', $app['islandora.idToUri']) + ->assert('id',$app['config']['islandora']['resourceIdRegex']) + ->before($app['islandora.hostHeaderNormalize']) + ->before($app['islandora.htmlHeaderToTurtle']) + ->value('id',"") + ->value('child',"") + ->bind('islandora.resourcePatch'); + $controllers->delete("/resource/{id}/{child}", "islandora.resourcecontroller:delete") + ->convert('id', $app['islandora.idToUri']) + ->assert('id',$app['config']['islandora']['resourceIdRegex']) + ->before($app['islandora.hostHeaderNormalize']) + ->before($app['islandora.htmlHeaderToTurtle']) + ->value('id',"") + ->value('child',"") + ->bind('islandora.resourceDelete'); + return $controllers; + } +} \ No newline at end of file diff --git a/services/ResourceServiceProvider/src/Provider/UUIDServiceProvider.php b/services/ResourceServiceProvider/src/Provider/UUIDServiceProvider.php new file mode 100644 index 000000000..c3433fa96 --- /dev/null +++ b/services/ResourceServiceProvider/src/Provider/UUIDServiceProvider.php @@ -0,0 +1,47 @@ +share(function() use ($app) { + if (!isset($app['UuidServiceProvider.default_namespace'])) { + throw new \RuntimeException('A Default Namespace is needed to generate UUID V5'); + } + $uuid5 = Uuid::uuid5(Uuid::NAMESPACE_DNS, $app['UuidServiceProvider.default_namespace']); + return $uuid5; + }); + + $app['islandora.uuid5'] = $app->protect(function($word, $namespace = NULL) use ($app) { + if (!isset($namespace)) { + $uuid5 = Uuid::uuid5($app['islandora.baseuuid5']->toString(), $word); + } + else { + $baseuuid5 = Uuid::uuid5(Uuid::NAMESPACE_DNS, $namespace); + $uuid5 = Uuid::uuid5($baseuuid5->toString(), $word); + } + return $uuid5->toString(); + }); + + $app['islandora.uuid4'] = $app->protect(function() use ($app) { + $uuid4 = Uuid::uuid4(); + return $uuid4->toString(); + }); + } + + function boot(Application $app) { + } +} \ No newline at end of file diff --git a/services/ResourceServiceProvider/src/index.php b/services/ResourceServiceProvider/src/index.php new file mode 100644 index 000000000..518fff3be --- /dev/null +++ b/services/ResourceServiceProvider/src/index.php @@ -0,0 +1,63 @@ +register(new \Silex\Provider\ServiceControllerServiceProvider()); +$app->register(new \Silex\Provider\TwigServiceProvider(), array( + 'twig.path' => __DIR__.'/../templates', +)); + +$islandoraResourceServiceProvider = new \Islandora\ResourceService\Provider\ResourceServiceProvider; + +$app->register($islandoraResourceServiceProvider); +$app->mount("/islandora", $islandoraResourceServiceProvider); + +/** + * Convert returned Guzzle responses to Symfony responses, type hinted. + */ +$app->view(function (ResponseInterface $psr7) { + return new Response($psr7->getBody(), $psr7->getStatusCode(), $psr7->getHeaders()); +}); + +$app->after(function (Request $request, Response $response, Application $app) { + // Todo a closing controller, not sure what now but i had an idea. +}); +$app->error(function (\Symfony\Component\HttpKernel\Exception\HttpException $e, $code) use ($app){ + if ($app['debug']) { + return; + } + return new Response(sprintf('Islandora Resource Service exception: %s / HTTP %d response', $e->getMessage(), $code), $code); +}); +$app->error(function (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, $code) use ($app){ + if ($app['debug']) { + return; + } + //Not sure what the best "verbose" message is + return new Response(sprintf('Islandora Resource Service exception: %s / HTTP %d response', $e->getMessage(), $code), $code); +}); +$app->error(function (\Exception $e, $code) use ($app){ + if ($app['debug']) { + return; + } + return new Response(sprintf('Islandora Resource Service uncatched exception: %s %d response', $e->getMessage(), $code), $code); +}); + + +$app->run(); diff --git a/services/ResourceServiceProvider/templates/getResourceByUUIDfromTS.sparql b/services/ResourceServiceProvider/templates/getResourceByUUIDfromTS.sparql new file mode 100644 index 000000000..b8fe9a70e --- /dev/null +++ b/services/ResourceServiceProvider/templates/getResourceByUUIDfromTS.sparql @@ -0,0 +1,5 @@ +PREFIX nfo: +PREFIX rdf: +SELECT ?s WHERE { + ?s nfo:uuid "{{uuid}}"^^ . +}