First draft
This commit is contained in:
23
composer.json
Normal file
23
composer.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"autoload" : {
|
||||
"psr-4" : {
|
||||
"App\\" : "src/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"symfony/http-foundation": "^6.1",
|
||||
"symfony/http-kernel": "^6.1",
|
||||
"symfony/routing": "^6.1",
|
||||
"symfony/dependency-injection": "^6.1",
|
||||
"symfony/string": "^6.1",
|
||||
"symfony/serializer": "^7.1",
|
||||
"symfony/property-access": "^7.1",
|
||||
"symfony/config": "^7.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"rector/rector": "^1.2",
|
||||
"phpunit/phpunit": "^10.5.63"
|
||||
}
|
||||
}
|
||||
5686
composer.lock
generated
Normal file
5686
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
125
src/Controller/OaiController.php
Normal file
125
src/Controller/OaiController.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Enum\OaiVerbs;
|
||||
use App\OAI\OaiError;
|
||||
use App\OAI\Verb\{
|
||||
GetRecord,
|
||||
Identify,
|
||||
ListRecords,
|
||||
ListIdentifiers,
|
||||
ListSets,
|
||||
ListMetadataFormats,
|
||||
};
|
||||
|
||||
final class OaiController
|
||||
{
|
||||
#[Route('/oai', methods: 'get')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$res = new Response();
|
||||
$res->headers->set('Content-Type', 'text/xml');
|
||||
$query = $request->getQueryString();
|
||||
$verb = $request->get('verb');
|
||||
|
||||
if ($query === null || $verb === null) {
|
||||
$error = new OaiError('badVerb', 'The verb argument is missing');
|
||||
$res->setContent($error->toXML());
|
||||
return $res;
|
||||
}
|
||||
|
||||
$validVerb = OaiVerbs::tryFrom($verb);
|
||||
|
||||
if (! $validVerb) {
|
||||
$error = new OaiError('badVerb', "'{$verb}' is not a valid OAI-PMH verb");
|
||||
$res->setContent($error->toXML());
|
||||
return $res;
|
||||
}
|
||||
|
||||
$mdPrefix = $request->get('metadataPrefix');
|
||||
$resumptionToken = $request->get('resumptionToken');
|
||||
|
||||
if (
|
||||
($validVerb === OaiVerbs::ListRecords
|
||||
|| $validVerb === OaiVerbs::ListIdentifiers
|
||||
|| $validVerb === OaiVerbs::GetRecord) &&
|
||||
($mdPrefix === null && $resumptionToken === null)
|
||||
) {
|
||||
$error = new OaiError('badArgument', 'The \'metadataPrefix\' argument is missing.');
|
||||
$res->setContent($error->toXML());
|
||||
return $res;
|
||||
}
|
||||
|
||||
return match ($validVerb->value) {
|
||||
'Identify' => $this->identify($res, (new Identify)->response()),
|
||||
'GetRecord' => $this->getRecord($request, $res),
|
||||
'ListRecords' => $this->listRecords($request, $res),
|
||||
'ListSets' => $this->listSets($res),
|
||||
'ListMetadataFormats' => $this->listMetadataFormats($res),
|
||||
'ListIdentifiers' => $this->listIdentifiers($request, $res),
|
||||
};
|
||||
}
|
||||
|
||||
private function identify(Response $res, string $content): Response
|
||||
{
|
||||
$res->setContent($content);
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function getRecord(Request $req, Response $res): Response
|
||||
{
|
||||
$identifier = $req->get('identifier');
|
||||
if (! $this->checkIdentifier($identifier)) {
|
||||
$error = new OaiError('idDoesNotExist', "The identifier '$identifier' is illegal.");
|
||||
$res->setContent($error->toXML());
|
||||
return $res;
|
||||
}
|
||||
|
||||
$format = $req->get('metadataPrefix');
|
||||
$res->setContent((new GetRecord($identifier, $format))->response());
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function listRecords(Request $req, Response $res): Response
|
||||
{
|
||||
$records = (new ListRecords($req))->response();
|
||||
$res->setContent($records);
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function checkIdentifier(string $identifier): bool
|
||||
{
|
||||
$pattern = "/oai:www.yourdomain.com:(article|model|image):\d+$/";
|
||||
return (bool)preg_match($pattern, $identifier);
|
||||
}
|
||||
|
||||
public function listSets(Response $res): Response
|
||||
{
|
||||
$xml = (new ListSets)->response();
|
||||
$res->setContent($xml);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function listMetadataFormats(Response $res): Response
|
||||
{
|
||||
$xml = (new ListMetadataFormats)->response();
|
||||
$res->setContent($xml);
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function listIdentifiers(Request $req, Response $res): Response
|
||||
{
|
||||
$xml = (new ListIdentifiers($req))->response();
|
||||
$res->setContent($xml);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
17
src/Enum/OaiSetspec.php
Normal file
17
src/Enum/OaiSetspec.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
/**
|
||||
* @todo Modify according to sets in DB or remove
|
||||
*/
|
||||
enum OaiSetspec: int
|
||||
{
|
||||
case JournalArticles = 2;
|
||||
case SupplementArticles = 5;
|
||||
case SupplementBook = 6;
|
||||
case Images = 8;
|
||||
case Models = 9;
|
||||
}
|
||||
15
src/Enum/OaiVerbs.php
Normal file
15
src/Enum/OaiVerbs.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum OaiVerbs: string
|
||||
{
|
||||
case Identify = 'Identify';
|
||||
case ListRecords = 'ListRecords';
|
||||
case ListIdentifiers = 'ListIdentifiers';
|
||||
case ListSets = 'ListSets';
|
||||
case ListMetadataFormats = 'ListMetadataFormats';
|
||||
case GetRecord = 'GetRecord';
|
||||
}
|
||||
14
src/OAI/Exception/IdentifierNotFoundException.php
Normal file
14
src/OAI/Exception/IdentifierNotFoundException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Exception;
|
||||
|
||||
final class IdentifierNotFoundException extends \Exception
|
||||
{
|
||||
public function __construct(string $identifier)
|
||||
{
|
||||
$this->message = "Identifier '{$identifier}' does not exist in this repository.";
|
||||
$this->code = 'idDoesNotExist';
|
||||
}
|
||||
}
|
||||
14
src/OAI/Exception/InvalidFormatException.php
Normal file
14
src/OAI/Exception/InvalidFormatException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Exception;
|
||||
|
||||
final class InvalidFormatException extends \Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->message = "The metadata format is not supported for this item.";
|
||||
$this->code = 'cannotDisseminateFormat';
|
||||
}
|
||||
}
|
||||
14
src/OAI/Exception/InvalidTokenException.php
Normal file
14
src/OAI/Exception/InvalidTokenException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Exception;
|
||||
|
||||
final class InvalidTokenException extends \Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->message = "The value of the resumptionToken argument is invalid or expired.";
|
||||
$this->code = 'badResumptionToken';
|
||||
}
|
||||
}
|
||||
71
src/OAI/OaiBase.php
Normal file
71
src/OAI/OaiBase.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
use \DOMDocument;
|
||||
|
||||
use function Symfony\Component\String\b;
|
||||
|
||||
/**
|
||||
* Creates the base XML document containing dynamically generated responses
|
||||
* @todo different create functions depending on request type? e.g. GetRecord
|
||||
*/
|
||||
final class OaiBase
|
||||
{
|
||||
/**
|
||||
* @param string $content The dynamically generated XML response
|
||||
* @param string $nodeName The root node name to be imported
|
||||
*/
|
||||
public static function create(string $content, string $nodeName, string $metadataFormat = 'oai_dc'): string
|
||||
{
|
||||
$main = new DOMDocument(encoding: 'UTF-8');
|
||||
$main->formatOutput = true;
|
||||
$xslt = $main->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="/xslt/oai2.xsl"');
|
||||
$main->appendChild($xslt);
|
||||
|
||||
$imported = new DOMDocument(encoding: 'UTF-8');
|
||||
$imported->loadXML(b($content)->toUnicodeString()->toString(), LIBXML_NOWARNING);
|
||||
$node = $imported->getElementsByTagName($nodeName)->item(0);
|
||||
|
||||
$root = $main->createElement('OAI-PMH');
|
||||
$root->setAttribute('xmlns', 'http://www.openarchives.org/OAI/2.0/');
|
||||
$root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
||||
$root->setAttribute('xsi:schemaLocation', 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd');
|
||||
$responseDate = $main->createElement('responseDate', date("Y-m-d\TH:i:s\Z"));
|
||||
$root->appendChild($responseDate);
|
||||
$request = $main->createElement('request', 'https://www.yourdomain.com/oai');
|
||||
|
||||
if ($nodeName !== 'error') {
|
||||
$request->setAttribute('verb', $nodeName);
|
||||
// TODO dynamic metadata prefix!
|
||||
$request->setAttribute('metadataPrefix', $metadataFormat);
|
||||
}
|
||||
|
||||
$root->appendChild($request);
|
||||
$main->appendChild($root);
|
||||
$node = $main->importNode($node, true);
|
||||
$root->appendChild($node);
|
||||
|
||||
// To avoid error when importing XML from RecordOaiDatacite... (why??)
|
||||
if ($metadataFormat === 'oai_datacite') {
|
||||
$oai_datacite = $main->getElementsByTagName('oai_datacite')->item(0);
|
||||
$resource = $main->getElementsByTagName('resource')->item(0);
|
||||
$xmlns = $main->createAttribute('xmlns');
|
||||
$xsi = $main->createAttribute('xsi:schemaLocation');
|
||||
$xmlnsRes = $main->createAttribute('xmlns');
|
||||
$xsiRes = $main->createAttribute('xsi:schemaLocation');
|
||||
$xsi->value = "http://schema.datacite.org/oai/oai-1.1/ http://schema.datacite.org/oai/oai-1.1/oai.xsd";
|
||||
$xmlns->value = "http://schema.datacite.org/oai/oai-1.1/";
|
||||
$oai_datacite->appendChild($xmlns);
|
||||
$oai_datacite->appendChild($xsi);
|
||||
$xmlnsRes->value = "http://datacite.org/schema/kernel-3";
|
||||
$xsiRes->value = "http://datacite.org/schema/kernel-3 http://schema.datacite.org/meta/kernel-3/metadata.xsd";
|
||||
$resource->appendChild($xmlnsRes);
|
||||
$resource->appendChild($xsiRes);
|
||||
}
|
||||
|
||||
return $main->saveXML();
|
||||
}
|
||||
}
|
||||
72
src/OAI/OaiError.php
Normal file
72
src/OAI/OaiError.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
/**
|
||||
* @todo Use Symfony serializer to generate XML?
|
||||
*/
|
||||
use App\OAI\OaiBase;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
|
||||
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
final class OaiError
|
||||
{
|
||||
#[SerializedName('@code')]
|
||||
public string $code;
|
||||
#[SerializedName('#')]
|
||||
public string $description;
|
||||
|
||||
public function __construct(
|
||||
$code,
|
||||
$description,
|
||||
private Serializer $serializer = new Serializer()
|
||||
)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->description = $description;
|
||||
|
||||
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
|
||||
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
|
||||
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
|
||||
public function setError(string $code, string $description)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize object
|
||||
* @todo Normalize to array then serialize to XML
|
||||
*/
|
||||
public function toXML(): string
|
||||
{
|
||||
$normal = $this->serializer->normalize($this);
|
||||
|
||||
$content = $this->serializer->encode(
|
||||
$normal,
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => 'error',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]);
|
||||
|
||||
return OaiBase::create($content, 'error');
|
||||
}
|
||||
}
|
||||
66
src/OAI/OaiIdentify.php
Normal file
66
src/OAI/OaiIdentify.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
use AC\DB\Connection;
|
||||
use App\OAI\RecordOaiInterface;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* Serializes an Identify response
|
||||
*/
|
||||
final class OaiIdentify implements RecordOaiInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Serializer $serializer = new Serializer(),
|
||||
)
|
||||
{
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer()],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* This should get literal values from settings / config
|
||||
*/
|
||||
public function toXML(): string
|
||||
{
|
||||
$datestamp = $this->getEarliestDatestamp();
|
||||
|
||||
$response = [
|
||||
'repositoryName' => 'Your bloody repo name',
|
||||
'baseURL' => "https://www.yourdomain.com/oai",
|
||||
'protocolVersion' => 2.0,
|
||||
'adminEmail' => 'crazyadmin@yourdomain.com',
|
||||
'earliestDatestamp' => $datestamp,
|
||||
'deletedRecord' => 'no',
|
||||
'granularity' => 'YYYY-MM-DD',
|
||||
];
|
||||
$content = $this->serializer->encode(
|
||||
$response,
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => 'Identify',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
/**
|
||||
* This should return the earliest datestamp from the repo's database
|
||||
* considering all available OAI resources
|
||||
*/
|
||||
private function getEarliestDatestamp(): string
|
||||
{
|
||||
// Implement
|
||||
return date('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
143
src/OAI/RecordOaiDataCite.php
Normal file
143
src/OAI/RecordOaiDataCite.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
use App\OAI\RecordOaiInterface;
|
||||
use App\OAI\Exception\IdentifierNotFoundException;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
|
||||
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
/**
|
||||
* Serializes a record (DB data) in the `oai_dacite` metadata format
|
||||
*/
|
||||
final class RecordOaiDataCite implements RecordOaiInterface
|
||||
{
|
||||
private string $identifier;
|
||||
private string $verb;
|
||||
|
||||
public array $titles;
|
||||
public array $creators;
|
||||
public array $rightsList;
|
||||
public array $descriptions;
|
||||
public array $dates;
|
||||
public int $publicationYear;
|
||||
public string $language;
|
||||
public array $resourceType;
|
||||
public array $subjects;
|
||||
#[SerializedName('identifier')]
|
||||
public array $oaiIdentifier;
|
||||
public array $alternateIdentifiers;
|
||||
public array $formats;
|
||||
public array|string $source;
|
||||
|
||||
public function __construct(
|
||||
string $identifier,
|
||||
string $verb = 'GetRecord',
|
||||
private Serializer $serializer = new Serializer(),
|
||||
)
|
||||
{
|
||||
$this->verb = $verb;
|
||||
$this->identifier = $identifier;
|
||||
preg_match("/oai:www.yourdomain.com(:|\/)(?P<res>.*):(?P<id>\d+)$/", $identifier, $matches);
|
||||
|
||||
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
|
||||
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
|
||||
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Map DB record data to object attributes for serialization
|
||||
*/
|
||||
private function mapData(array $record): void
|
||||
{
|
||||
// Implement
|
||||
}
|
||||
/**
|
||||
* Return an XML representation of a DB record
|
||||
* in a given format
|
||||
* @throws IdentifierNotFoundException
|
||||
*/
|
||||
public function toXML(): string
|
||||
{
|
||||
$record = []; // This represents a record from the database
|
||||
if ($record === null) {
|
||||
throw new IdentifierNotFoundException($this->identifier);
|
||||
}
|
||||
$this->mapData($record);
|
||||
$normal = $this->serializer->normalize($this);
|
||||
|
||||
$data = $normal;
|
||||
$data = $this->buildRelated($record, $data);
|
||||
|
||||
$oai_datacite = [
|
||||
'oai_datacite' => [
|
||||
['resource' => $data]
|
||||
]
|
||||
];
|
||||
$header = [
|
||||
'identifier' => $this->identifier,
|
||||
'datestamp' => $record['datestamp'],
|
||||
'setSpec' => $record['setSpec'],
|
||||
];
|
||||
// Adapt for ListRecords...
|
||||
$record = $this->verb === 'GetRecord' ?
|
||||
['record' => ['header' => $header, 'metadata' => $oai_datacite]] :
|
||||
['header' => $header, 'metadata' => $oai_datacite];
|
||||
|
||||
$content = $this->serializer->encode(
|
||||
$record,
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => $this->verb === 'GetRecord' ? $this->verb : 'record',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return $content;
|
||||
}
|
||||
/**
|
||||
* Example of creating related identifiers for a DataCite record
|
||||
*/
|
||||
private function buildRelated(array $record, array $data)
|
||||
{
|
||||
if (str_contains($this->identifier, 'image')) {
|
||||
$models = $record['relation']['related'];
|
||||
$relModels = [];
|
||||
foreach ($models as $mod) {
|
||||
array_push(
|
||||
$relModels,
|
||||
[
|
||||
'@relatedIdentifierType' => 'URL',
|
||||
'@relationType' => 'IsDerivedFrom',
|
||||
'#' => $mod
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$data['#']['relatedIdentifiers']['relatedIdentifier'] = [
|
||||
[
|
||||
'@relatedIdentifierType' => 'DOI',
|
||||
'@relationType' => 'IsPartOf',
|
||||
'#' => preg_replace("/^info(.*)(10.*$)/i", '$2', $record['relation'][1]),
|
||||
],
|
||||
...$relModels
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
123
src/OAI/RecordOaiDc.php
Normal file
123
src/OAI/RecordOaiDc.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
use App\OAI\RecordOaiInterface;
|
||||
use App\OAI\Exception\IdentifierNotFoundException;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
|
||||
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
|
||||
/**
|
||||
* Serializes a record (DB data) in the `oai_dc` metadata format
|
||||
* @class RecordOaiDc
|
||||
*/
|
||||
final class RecordOaiDc implements RecordOaiInterface
|
||||
{
|
||||
private int $id;
|
||||
private string $identifier;
|
||||
private string $verb;
|
||||
|
||||
#[SerializedName('dc:title')]
|
||||
public string $title;
|
||||
#[SerializedName('dc:rights')]
|
||||
public array $rights;
|
||||
#[SerializedName('dc:description')]
|
||||
public ?string $description;
|
||||
#[SerializedName('dc:date')]
|
||||
public int $date;
|
||||
#[SerializedName('dc:language')]
|
||||
public string $language;
|
||||
#[SerializedName('dc:type')]
|
||||
public array $recordType;
|
||||
#[SerializedName('dc:subject')]
|
||||
public array $subject;
|
||||
#[SerializedName('dc:creator')]
|
||||
public $creator;
|
||||
#[SerializedName('dc:identifier')]
|
||||
public array $oaiIdentifier;
|
||||
#[SerializedName('dc:format')]
|
||||
public string $format;
|
||||
#[SerializedName('dc:source')]
|
||||
public array|string $source;
|
||||
|
||||
public function __construct(
|
||||
string $identifier,
|
||||
string $verb = 'GetRecord',
|
||||
private Serializer $serializer = new Serializer(),
|
||||
)
|
||||
{
|
||||
$this->verb = $verb;
|
||||
$this->identifier = $identifier;
|
||||
preg_match("/oai:www.yourdomain.com(:|\/)(?P<res>.*):(?P<id>\d+)$/", $identifier, $matches);
|
||||
|
||||
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
|
||||
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
|
||||
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Map DB record data to object attributes for serialization
|
||||
*/
|
||||
private function mapData(array $record): void
|
||||
{
|
||||
// Implement
|
||||
}
|
||||
/**
|
||||
* Return an XML representation of a DB record
|
||||
* in a given format
|
||||
*/
|
||||
public function toXML(): string
|
||||
{
|
||||
$record = []; // This represents a record from the database
|
||||
if ($record === null) {
|
||||
throw new IdentifierNotFoundException($this->identifier);
|
||||
}
|
||||
$this->mapData($record);
|
||||
$normal = $this->serializer->normalize($this);
|
||||
|
||||
$data = [
|
||||
'@xmlns:oai_dc' => "http://www.openarchives.org/OAI/2.0/oai_dc/",
|
||||
'@xmlns:dc' => "http://purl.org/dc/elements/1.1/",
|
||||
'#' => $normal
|
||||
];
|
||||
|
||||
$oai_dc = ['oai_dc:dc' => $data];
|
||||
if (isset($record['relation'])) {
|
||||
$oai_dc['oai_dc:dc']['dc:relation'] = $record['relation'];
|
||||
}
|
||||
$header = [
|
||||
'identifier' => $this->identifier,
|
||||
'datestamp' => $record['datestamp'],
|
||||
'setSpec' => $record['setSpec'],
|
||||
];
|
||||
// Adapt for ListRecords...
|
||||
$record = $this->verb === 'GetRecord' ?
|
||||
['record' => ['header' => $header, 'metadata' => $oai_dc]] :
|
||||
['header' => $header, 'metadata' => $oai_dc];
|
||||
|
||||
$content = $this->serializer->encode(
|
||||
$record,
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => $this->verb === 'GetRecord' ? $this->verb : 'record',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
10
src/OAI/RecordOaiInterface.php
Normal file
10
src/OAI/RecordOaiInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
interface RecordOaiInterface
|
||||
{
|
||||
public function toXML(): string;
|
||||
}
|
||||
185
src/OAI/ResumptionToken.php
Normal file
185
src/OAI/ResumptionToken.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI;
|
||||
|
||||
use App\OAI\Exception\InvalidTokenException;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Serializer\Attribute\Ignore;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use \Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use \Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
|
||||
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
|
||||
|
||||
final class ResumptionToken
|
||||
{
|
||||
#[SerializedName('@cursor')]
|
||||
public readonly int $cursor;
|
||||
#[SerializedName('@completeListSize')]
|
||||
public readonly int $size;
|
||||
#[Ignore]
|
||||
public readonly \DateTimeImmutable $validity;
|
||||
#[SerializedName('@expirationDate')]
|
||||
public readonly string $expirationDate;
|
||||
#[Ignore]
|
||||
public readonly string $metadataFormat;
|
||||
private Serializer $serializer;
|
||||
#[Ignore]
|
||||
public readonly ?string $set;
|
||||
#[Ignore]
|
||||
public readonly ?string $from;
|
||||
#[Ignore]
|
||||
public readonly ?string $until;
|
||||
|
||||
public function __construct(
|
||||
int $cursor,
|
||||
int $size,
|
||||
string $metadataFormat = 'oai_dc', // Default to Dublin Core
|
||||
?string $set = null,
|
||||
?string $from = null,
|
||||
?string $until = null
|
||||
)
|
||||
{
|
||||
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
|
||||
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter)],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
|
||||
$now = new \DateTimeImmutable;
|
||||
$this->cursor = $cursor;
|
||||
$this->size = $size;
|
||||
// TODO Hard-coded 4 days added to now for validity
|
||||
$this->validity = $now->add(new \DateInterval('P4D'));
|
||||
$this->expirationDate = $this->validity->format('Y-m-d');
|
||||
$this->metadataFormat = $metadataFormat;
|
||||
$this->set = $set;
|
||||
$this->from = $from;
|
||||
$this->until = $until;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->encode();
|
||||
}
|
||||
|
||||
public function toXML(): string
|
||||
{
|
||||
$normal = $this->serializer->normalize($this);
|
||||
$normal['#'] = $this->__toString();
|
||||
|
||||
$token = $this->serializer->encode(
|
||||
$normal,
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => 'resumptionToken',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo sets and formats from DB...
|
||||
*/
|
||||
public function encode(): string
|
||||
{
|
||||
$hexCurs = dechex($this->cursor);
|
||||
$hexSize = dechex($this->size);
|
||||
$hexTime = dechex($this->validity->getTimestamp());
|
||||
$from = $this->from !== null ?
|
||||
dechex((new DateTimeImmutable($this->from))->getTimestamp()) : 0;
|
||||
$until = $this->until !== null ?
|
||||
dechex((new DateTimeImmutable($this->until))->getTimestamp()) : 0;
|
||||
|
||||
$setMap = match ($this->set) {
|
||||
null => 0,
|
||||
'J:A' => 1,
|
||||
'J:B' => 2,
|
||||
'S:A' => 3,
|
||||
'S:B' => 4,
|
||||
'openaire' => 5,
|
||||
'openaire_data' => 6,
|
||||
'images' => 7,
|
||||
'models' => 8,
|
||||
default => 0
|
||||
};
|
||||
|
||||
$format = match ($this->metadataFormat) {
|
||||
'oai_dc' => 1,
|
||||
'oai_datacite' => 2,
|
||||
'oai_openaire' => 3,
|
||||
};
|
||||
|
||||
return "{$hexCurs}-{$hexSize}-{$hexTime}-{$setMap}-$format-$from-$until";
|
||||
}
|
||||
/**
|
||||
* @throws InvalidTokenException
|
||||
*/
|
||||
public static function decode(string $token): ResumptionToken
|
||||
{
|
||||
$parts = explode('-', $token);
|
||||
$cursor = hexdec($parts[0]);
|
||||
$size = hexdec($parts[1]);
|
||||
$validity = hexdec($parts[2]);
|
||||
|
||||
$set = match ((int)$parts[3]) {
|
||||
0 => null,
|
||||
1 => 'J:A',
|
||||
2 => 'J:B',
|
||||
3 => 'S:A',
|
||||
4 => 'S:B',
|
||||
5 => 'openaire',
|
||||
6 => 'openaire_data',
|
||||
7 => 'images',
|
||||
8 => 'models',
|
||||
default => null
|
||||
};
|
||||
|
||||
$format = match ((int)$parts[4]) {
|
||||
1 => 'oai_dc',
|
||||
2 => 'oai_datacite',
|
||||
3 =>'oai_openaire',
|
||||
default => 'oai_dc',
|
||||
};
|
||||
|
||||
$from = hexdec($parts[5]);
|
||||
$from = $from === 0 ?
|
||||
null : (new DateTime)->setTimestamp($from)->format('Y-m-d');
|
||||
$until = hexdec($parts[6]);
|
||||
$until = $until === 0 ?
|
||||
null : (new DateTime)->setTimestamp($until)->format('Y-m-d');
|
||||
|
||||
if (! ResumptionToken::validate($validity)) {
|
||||
throw new InvalidTokenException;
|
||||
}
|
||||
|
||||
return new static(
|
||||
$cursor,
|
||||
$size,
|
||||
$format,
|
||||
set: $set,
|
||||
from: $from,
|
||||
until: $until
|
||||
);
|
||||
}
|
||||
|
||||
private static function validate(int $timestamp): bool
|
||||
{
|
||||
$now = new \DateTimeImmutable;
|
||||
$validity = \DateTimeImmutable::createFromFormat('U', (string)$timestamp);
|
||||
|
||||
return $validity->diff($now)->days >= 0;
|
||||
}
|
||||
}
|
||||
65
src/OAI/Verb/GetRecord.php
Normal file
65
src/OAI/Verb/GetRecord.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\{
|
||||
RecordOaiDc,
|
||||
RecordOaiDataCite,
|
||||
RecordOaiInterface
|
||||
};
|
||||
use App\OAI\Exception\IdentifierNotFoundException;
|
||||
use App\OAI\Exception\InvalidFormatException;
|
||||
use App\OAI\OaiBase;
|
||||
use App\OAI\OaiError;
|
||||
|
||||
/**
|
||||
* Metadata for a single OAI record
|
||||
*/
|
||||
final class GetRecord
|
||||
{
|
||||
public readonly RecordOaiInterface $record;
|
||||
private ?OaiError $error = null;
|
||||
private string $metadataFormat;
|
||||
|
||||
public function __construct(
|
||||
string $identifier,
|
||||
string $metadataFormat,
|
||||
)
|
||||
{
|
||||
$this->metadataFormat = $metadataFormat;
|
||||
$record = null;
|
||||
|
||||
try {
|
||||
$record = match ($metadataFormat) {
|
||||
'oai_dc' => new RecordOaiDc($identifier),
|
||||
'oai_datacite' => new RecordOaiDataCite($identifier),
|
||||
default => new RecordOaiDc($identifier),
|
||||
};
|
||||
}
|
||||
catch (InvalidFormatException $e) {
|
||||
$this->error = new OaiError($e->getCode(),$e->getMessage());
|
||||
}
|
||||
|
||||
$this->record = $record;
|
||||
}
|
||||
/**
|
||||
* Returns the XML response (GetRecord or OaiError)
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
if ($this->error) {
|
||||
return $this->error->toXML();
|
||||
}
|
||||
|
||||
try {
|
||||
$xml = $this->record->toXML();
|
||||
return OaiBase::create($xml, 'GetRecord', $this->metadataFormat);
|
||||
}
|
||||
catch (IdentifierNotFoundException $e) {
|
||||
return (new OaiError($e->getCode(),$e->getMessage()))
|
||||
->toXML();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/OAI/Verb/Identify.php
Normal file
29
src/OAI/Verb/Identify.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\OaiIdentify;
|
||||
use App\OAI\OaiBase;
|
||||
|
||||
/**
|
||||
* Identify data for the repository
|
||||
*/
|
||||
final class Identify
|
||||
{
|
||||
public readonly OaiIdentify $identify;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->identify = new OaiIdentify;
|
||||
}
|
||||
/**
|
||||
* Returns the XML response
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
$xml = $this->identify->toXML();
|
||||
return OaiBase::create($xml, 'Identify');
|
||||
}
|
||||
}
|
||||
154
src/OAI/Verb/ListIdentifiers.php
Normal file
154
src/OAI/Verb/ListIdentifiers.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\ResumptionToken;
|
||||
use App\OAI\Exception\InvalidTokenException;
|
||||
use App\OAI\OaiBase;
|
||||
use App\OAI\OaiError;
|
||||
use AC\DB\Connection;
|
||||
use \Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
use function \Symfony\Component\String\b;
|
||||
|
||||
/**
|
||||
* Represents a ListIdentifiers response
|
||||
*/
|
||||
final class ListIdentifiers
|
||||
{
|
||||
private Request $request;
|
||||
private ?string $metadataFormat;
|
||||
private ?ResumptionToken $resumptionToken;
|
||||
|
||||
public function __construct(Request $req)
|
||||
{
|
||||
$this->request = $req;
|
||||
}
|
||||
/**
|
||||
* Returns the XML response
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
$this->metadataFormat = $this->request->get('metadataPrefix');
|
||||
$until = $this->request->get('until');
|
||||
$from = $this->request->get('from');
|
||||
$set = $this->request->get('set');
|
||||
$token = $this->request->get('resumptionToken');
|
||||
|
||||
if ($token !== null) {
|
||||
$tokenObj = ResumptionToken::decode($token);
|
||||
$set = $tokenObj->set;
|
||||
$this->metadataFormat = $tokenObj->metadataFormat;
|
||||
$from = $tokenObj->from;
|
||||
$until = $tokenObj->until;
|
||||
}
|
||||
|
||||
$hasErrors = $this->hasErrors($from, $until, $set);
|
||||
|
||||
if ($hasErrors !== false) return $hasErrors->toXML();
|
||||
|
||||
try {
|
||||
$identifiers = $this->getIdentifiers(
|
||||
$set,
|
||||
$from,
|
||||
$until,
|
||||
);
|
||||
}
|
||||
catch (InvalidTokenException $e) {
|
||||
return (new OaiError($e->getCode(), $e->getMessage()))
|
||||
->toXML();
|
||||
}
|
||||
|
||||
if (count($identifiers) === 0) {
|
||||
return (new OaiError('noRecordsMatch', 'No records were found in repository.'))
|
||||
->toXML();
|
||||
}
|
||||
|
||||
$xml = '';
|
||||
|
||||
$xml = $this->createXML($identifiers);
|
||||
$xml = b($xml)->toUnicodeString()->trim("\n");
|
||||
$xml = $xml->prepend("<ListIdentifiers>\n");
|
||||
$xml = $xml->append("\n</ListIdentifiers>");
|
||||
|
||||
return OaiBase::create($xml->toString(), 'ListIdentifiers', $this->metadataFormat);
|
||||
}
|
||||
|
||||
private function checkGranularity(string $date): bool
|
||||
{
|
||||
$valid = true;
|
||||
$pattern = "/\d{4}-\d{2}-\d{2}/";
|
||||
preg_match($pattern, $date, $matches);
|
||||
|
||||
if (count($matches) === 0) return false;
|
||||
|
||||
$valid &= $matches[0] === $date;
|
||||
$parts = explode('-', $date);
|
||||
$valid &= checkdate((int)$parts[1], (int)$parts[2], (int)$parts[0]);
|
||||
|
||||
return (bool)$valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should check the given setspec against the available sets
|
||||
* from the database
|
||||
*/
|
||||
private function checkValidSet(string $setspec): bool
|
||||
{
|
||||
// Implement
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should return the identifiers of records in the DB based on
|
||||
* the given filters (set, from, until)
|
||||
* @throws InvalidTokenException
|
||||
* @return string[]
|
||||
*/
|
||||
private function getIdentifiers(?string $set, ?string $from, ?string $until,): array
|
||||
{
|
||||
// Implement
|
||||
return ['first_identifier', 'second_identifier'];
|
||||
}
|
||||
|
||||
private function createXML(array $identifiers): string
|
||||
{
|
||||
$xml = '';
|
||||
foreach ($identifiers as $id) {
|
||||
$xml .= "<header>\n";
|
||||
$xml .= "<identifier>{$id['identifier']}</identifier>\n";
|
||||
$xml .= "<datestamp>{$id['datestamp']}</datestamp>\n";
|
||||
$xml .= "<setSpec>{$id['setSpec']}</setSpec>\n";
|
||||
$xml .= "</header>";
|
||||
}
|
||||
|
||||
if ($this->resumptionToken->cursor < $this->resumptionToken->size) {
|
||||
$xml .= "\n{$this->resumptionToken->toXML()}";
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function hasErrors(?string $from, ?string $until, ?string $set): OaiError|false
|
||||
{
|
||||
$error = false;
|
||||
|
||||
if ($from !== null && ! $this->checkGranularity($from) ||
|
||||
$until !== null && ! $this->checkGranularity($until)
|
||||
) {
|
||||
$error = new OaiError('badArgument', "The argument value for 'from' and/or 'until' is invalid.");
|
||||
}
|
||||
|
||||
if (($until !== null && $from !== null) && ($from > $until)) {
|
||||
$error = new OaiError('noRecordsMatch', 'No records were found in repository.');
|
||||
}
|
||||
|
||||
if ($set !== null && ! $this->checkValidSet($set)) {
|
||||
$error = new OaiError('badArgument', "The set '$set' does not exist in this repository.");
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
68
src/OAI/Verb/ListMetadataFormats.php
Normal file
68
src/OAI/Verb/ListMetadataFormats.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\OaiBase;
|
||||
use App\OAI\OaiError;
|
||||
use AC\DB\Connection;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* Represents a ListMetadataFormats response
|
||||
*/
|
||||
final class ListMetadataFormats
|
||||
{
|
||||
public function __construct(
|
||||
private Serializer $serializer = new Serializer(),
|
||||
)
|
||||
{
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer()],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns the XML response
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
$formats = $this->getValidFormats();
|
||||
|
||||
$content = $this->serializer->encode(
|
||||
['metadataFormat' => [...$formats]],
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => 'ListMetadataFormats',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return OaiBase::create($content, 'ListMetadataFormats');
|
||||
}
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function getValidFormats(): array
|
||||
{
|
||||
$dbConn = Connection::new();
|
||||
$queryBuilder = $dbConn->createQueryBuilder();
|
||||
|
||||
$formats = $queryBuilder->select(
|
||||
'prefix as metadataPrefix',
|
||||
'schemaLoc as schema',
|
||||
'namespace as metadataNamespace',
|
||||
)
|
||||
->from('metadata_formats')
|
||||
->fetchAllAssociative();
|
||||
|
||||
return $formats;
|
||||
}
|
||||
}
|
||||
186
src/OAI/Verb/ListRecords.php
Normal file
186
src/OAI/Verb/ListRecords.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\{
|
||||
RecordOaiDc,
|
||||
RecordOaiDataCite,
|
||||
RecordOaiInterface,
|
||||
ResumptionToken
|
||||
};
|
||||
use App\OAI\Exception\InvalidTokenException;
|
||||
use App\OAI\OaiBase;
|
||||
use App\OAI\OaiError;
|
||||
use \Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
use function \Symfony\Component\String\b;
|
||||
|
||||
/**
|
||||
* Represents a ListRecords response
|
||||
*/
|
||||
final class ListRecords
|
||||
{
|
||||
private RecordOaiInterface $record;
|
||||
private Request $request;
|
||||
private ?string $metadataFormat;
|
||||
private ?ResumptionToken $resumptionToken;
|
||||
|
||||
public function __construct(Request $req)
|
||||
{
|
||||
$this->request = $req;
|
||||
}
|
||||
/**
|
||||
* Returns the XML response
|
||||
* @todo Refactor (complexity...)
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
$this->metadataFormat = $this->request->get('metadataPrefix');
|
||||
$until = $this->request->get('until');
|
||||
$from = $this->request->get('from');
|
||||
$set = $this->request->get('set');
|
||||
$token = $this->request->get('resumptionToken');
|
||||
|
||||
if ($token !== null) {
|
||||
$tokenObj = ResumptionToken::decode($token);
|
||||
$set = $tokenObj->set;
|
||||
$this->metadataFormat = $tokenObj->metadataFormat;
|
||||
$from = $tokenObj->from;
|
||||
$until = $tokenObj->until;
|
||||
}
|
||||
|
||||
$hasErrors = $this->hasErrors($from, $until, $set);
|
||||
|
||||
if ($hasErrors !== false) return $hasErrors->toXML();
|
||||
|
||||
try {
|
||||
$identifiers = $this->getIdentifiers(
|
||||
$set,
|
||||
$from,
|
||||
$until,
|
||||
);
|
||||
}
|
||||
catch (InvalidTokenException $e) {
|
||||
return (new OaiError($e->getCode(), $e->getMessage()))
|
||||
->toXML();
|
||||
}
|
||||
|
||||
if (count($identifiers) === 0) {
|
||||
return (new OaiError('noRecordsMatch', 'No records were found in repository.'))
|
||||
->toXML();
|
||||
}
|
||||
|
||||
$xml = '';
|
||||
|
||||
$xml = $this->createXML($identifiers, $this->metadataFormat);
|
||||
$xml = b($xml)->toUnicodeString()->trim("\n");
|
||||
$xml = $xml->prepend("<ListRecords>\n");
|
||||
$xml = $xml->append("\n</ListRecords>");
|
||||
|
||||
return OaiBase::create($xml->toString(), 'ListRecords', $this->metadataFormat);
|
||||
}
|
||||
|
||||
private function checkGranularity(string $date): bool
|
||||
{
|
||||
$valid = true;
|
||||
$pattern = "/\d{4}-\d{2}-\d{2}/";
|
||||
preg_match($pattern, $date, $matches);
|
||||
|
||||
if (count($matches) === 0) return false;
|
||||
|
||||
$valid &= $matches[0] === $date;
|
||||
$parts = explode('-', $date);
|
||||
$valid &= checkdate((int)$parts[1], (int)$parts[2], (int)$parts[0]);
|
||||
|
||||
return (bool)$valid;
|
||||
}
|
||||
|
||||
private function checkValidSet(string $setspec): bool
|
||||
{
|
||||
// Implement
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Implement limit for cumulative array
|
||||
* @throws InvalidTokenException
|
||||
* @return string[]
|
||||
*/
|
||||
private function getIdentifiers(?string $set, ?string $from, ?string $until,): array
|
||||
{
|
||||
$identifiers = [];
|
||||
$decodedToken = null;
|
||||
$offset = 0;
|
||||
$size = 0;
|
||||
$limit = (int)$_ENV['OAI_LIMIT'];
|
||||
$token = $this->request->get('resumptionToken');
|
||||
|
||||
if ($token !== null) {
|
||||
$decodedToken = ResumptionToken::decode($token);
|
||||
$offset = $decodedToken->cursor;
|
||||
}
|
||||
|
||||
if ($set === null && $this->metadataFormat === 'oai_dc') {
|
||||
// Get resources and size (total number)
|
||||
|
||||
$this->resumptionToken = new ResumptionToken(
|
||||
$offset + $limit,
|
||||
$size,
|
||||
set: $set,
|
||||
metadataFormat: $this->metadataFormat,
|
||||
from: $from,
|
||||
until: $until,
|
||||
);
|
||||
|
||||
return $identifiers;
|
||||
}
|
||||
|
||||
return $identifiers;
|
||||
}
|
||||
|
||||
private function createXML(
|
||||
array $identifiers,
|
||||
string $metadataFormat,
|
||||
): string
|
||||
{
|
||||
$xml = '';
|
||||
|
||||
foreach ($identifiers as $id) {
|
||||
$this->record = match ($metadataFormat) {
|
||||
'oai_dc' => new RecordOaiDc($id['identifier'], verb: 'ListRecords'),
|
||||
'oai_datacite' => new RecordOaiDataCite($id['identifier'], verb: 'ListRecords'),
|
||||
default => new RecordOaiDc($id['identifier'], verb: 'ListRecords'),
|
||||
};
|
||||
$xml .= "\n" . $this->record->toXML();
|
||||
}
|
||||
|
||||
if ($this->resumptionToken->cursor < $this->resumptionToken->size) {
|
||||
$xml .= "\n{$this->resumptionToken->toXML()}";
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function hasErrors(?string $from, ?string $until, ?string $set): OaiError|false
|
||||
{
|
||||
$error = false;
|
||||
|
||||
if ($from !== null && ! $this->checkGranularity($from) ||
|
||||
$until !== null && ! $this->checkGranularity($until)
|
||||
) {
|
||||
$error = new OaiError('badArgument', "The argument value for 'from' and/or 'until' is invalid.");
|
||||
}
|
||||
|
||||
if (($until !== null && $from !== null) && ($from > $until)) {
|
||||
$error = new OaiError('noRecordsMatch', 'No records were found in repository.');
|
||||
}
|
||||
|
||||
if ($set !== null && ! $this->checkValidSet($set)) {
|
||||
$error = new OaiError('badArgument', "The set '$set' does not exist in this repository.");
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
57
src/OAI/Verb/ListSets.php
Normal file
57
src/OAI/Verb/ListSets.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\OAI\Verb;
|
||||
|
||||
use App\OAI\OaiBase;
|
||||
use App\OAI\OaiError;
|
||||
use AC\DB\Connection;
|
||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* Represents a ListSets response
|
||||
*/
|
||||
final class ListSets
|
||||
{
|
||||
public function __construct(
|
||||
private Serializer $serializer = new Serializer(),
|
||||
)
|
||||
{
|
||||
$this->serializer = new Serializer(
|
||||
[new ObjectNormalizer()],
|
||||
[new XmlEncoder()]
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns the XML response
|
||||
*/
|
||||
public function response(): string
|
||||
{
|
||||
$sets = $this->getValidSets();
|
||||
|
||||
$content = $this->serializer->encode(
|
||||
['set' => [...$sets]],
|
||||
'xml',
|
||||
[
|
||||
'xml_format_output' => true,
|
||||
'xml_root_node_name' => 'ListSets',
|
||||
// Don't include the XML declaration, handled by OaiBase
|
||||
'encoder_ignored_node_types' => [
|
||||
\XML_PI_NODE,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return OaiBase::create($content, 'ListSets');
|
||||
}
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function getValidSets(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user