Refactored and cleaned code
This commit is contained in:
parent
d16efe47ed
commit
97fee92f63
|
@ -7,20 +7,35 @@
|
||||||
xsi:noNamespaceSchemaLocation="
|
xsi:noNamespaceSchemaLocation="
|
||||||
http://pmd.sf.net/ruleset_xml_schema.xsd">
|
http://pmd.sf.net/ruleset_xml_schema.xsd">
|
||||||
<description>
|
<description>
|
||||||
Open Culture Consulting follows PHP Mess Detector standards.
|
Open Culture Consulting follows PHP Mess Detector standards with few exceptions.
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<rule ref="rulesets/cleancode.xml">
|
<rule ref="rulesets/cleancode.xml">
|
||||||
|
<!--
|
||||||
|
We sometimes want to use else expressions for better readability.
|
||||||
|
-->
|
||||||
<exclude name="ElseExpression" />
|
<exclude name="ElseExpression" />
|
||||||
<!-- We need to statically access third-party helpers from Symfony. -->
|
|
||||||
<exclude name="StaticAccess" />
|
<exclude name="StaticAccess" />
|
||||||
|
<exclude name="BooleanArgumentFlag" />
|
||||||
</rule>
|
</rule>
|
||||||
<rule ref="rulesets/codesize.xml" />
|
<rule ref="rulesets/codesize.xml" />
|
||||||
<rule ref="rulesets/controversial.xml" />
|
<rule ref="rulesets/controversial.xml" />
|
||||||
<rule ref="rulesets/design.xml" />
|
<rule ref="rulesets/design.xml" />
|
||||||
<rule ref="rulesets/naming.xml" />
|
<rule ref="rulesets/naming.xml">
|
||||||
|
<exclude name="ShortVariable" />
|
||||||
|
</rule>
|
||||||
|
<rule ref="rulesets/naming.xml/ShortVariable">
|
||||||
|
<properties>
|
||||||
|
<!--
|
||||||
|
We want to allow shorter variable names as long as they are self-explanatory.
|
||||||
|
-->
|
||||||
|
<property name="minimum" value="2" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
<rule ref="rulesets/unusedcode.xml">
|
<rule ref="rulesets/unusedcode.xml">
|
||||||
<!-- We have to declare unused parameters to satisfy interface requirements. -->
|
<!--
|
||||||
|
We have to declare unused parameters to satisfy interface requirements.
|
||||||
|
-->
|
||||||
<exclude name="UnusedFormalParameter" />
|
<exclude name="UnusedFormalParameter" />
|
||||||
</rule>
|
</rule>
|
||||||
</ruleset>
|
</ruleset>
|
||||||
|
|
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
office@opencultureconsulting.com.
|
<office@opencultureconsulting.com>.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
Please read the excellent [GitHub Open Source Guide](https://opensource.guide/how-to-contribute/) on *How to Contribute on Open Source*.
|
Please read the excellent [GitHub Open Source Guide][guide] on *How to
|
||||||
|
Contribute on Open Source*.
|
||||||
|
|
||||||
If you have any further questions just [open a new issue](https://github.com/opencultureconsulting/oai-pmh2/issues/new) and I'll be happy to assist!
|
[guide]: <https://opensource.guide/how-to-contribute/>
|
||||||
|
|
||||||
|
If you have any further questions just [open a new issue][issuetracker] and
|
||||||
|
I'll be happy to assist!
|
||||||
|
|
||||||
|
[issuetracker]: <https://github.com/opencultureconsulting/oai-pmh2/issues/new>
|
||||||
|
|
2
bin/cli
2
bin/cli
|
@ -49,7 +49,7 @@ $commands = [
|
||||||
try {
|
try {
|
||||||
ConsoleRunner::run(
|
ConsoleRunner::run(
|
||||||
new SingleManagerProvider(
|
new SingleManagerProvider(
|
||||||
Database::getInstance()->getEntityManager()
|
EntityManager::getInstance()
|
||||||
),
|
),
|
||||||
$commands
|
$commands
|
||||||
);
|
);
|
||||||
|
|
|
@ -31,10 +31,10 @@
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
"ext-libxml": "*",
|
"ext-libxml": "*",
|
||||||
"ext-sqlite3": "*",
|
"ext-sqlite3": "*",
|
||||||
"doctrine/dbal": "^3.8",
|
"doctrine/dbal": "^4.1",
|
||||||
"doctrine/orm": "^3.2",
|
"doctrine/orm": "^3.2",
|
||||||
"opencultureconsulting/basics": "^1.1",
|
"opencultureconsulting/basics": "^2.1",
|
||||||
"opencultureconsulting/psr15": "^1.0",
|
"opencultureconsulting/psr15": "^1.2",
|
||||||
"symfony/cache": "^6.4",
|
"symfony/cache": "^6.4",
|
||||||
"symfony/console": "^6.4",
|
"symfony/console": "^6.4",
|
||||||
"symfony/filesystem": "^6.4",
|
"symfony/filesystem": "^6.4",
|
||||||
|
@ -44,11 +44,13 @@
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpdocumentor/shim": "^3.5",
|
"phpdocumentor/shim": "^3.5",
|
||||||
"phpmd/phpmd": "^2.15",
|
"phpmd/phpmd": "^2.15",
|
||||||
"phpstan/phpstan": "^1.11",
|
"phpstan/phpstan": "^1.12",
|
||||||
|
"phpstan/phpstan-doctrine": "^1.5",
|
||||||
"phpstan/phpstan-strict-rules": "^1.6",
|
"phpstan/phpstan-strict-rules": "^1.6",
|
||||||
"friendsofphp/php-cs-fixer": "^3.59",
|
"phpstan/phpstan-symfony": "^1.4",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.64",
|
||||||
"squizlabs/php_codesniffer": "^3.10",
|
"squizlabs/php_codesniffer": "^3.10",
|
||||||
"vimeo/psalm": "^5.25"
|
"vimeo/psalm": "^5.26"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
@ -74,7 +76,7 @@
|
||||||
],
|
],
|
||||||
"doctrine:clear-cache": [
|
"doctrine:clear-cache": [
|
||||||
"@php bin/cli orm:clear-cache:metadata --flush",
|
"@php bin/cli orm:clear-cache:metadata --flush",
|
||||||
"@php bin/cli orm:clear-cache:query --flush",
|
"@php bin/cli orm:clear-cache:query",
|
||||||
"@php bin/cli orm:clear-cache:result --flush"
|
"@php bin/cli orm:clear-cache:result --flush"
|
||||||
],
|
],
|
||||||
"doctrine:initialize-database": [
|
"doctrine:initialize-database": [
|
||||||
|
@ -95,7 +97,7 @@
|
||||||
],
|
],
|
||||||
"phpmd:check": [
|
"phpmd:check": [
|
||||||
"@php -r \"if (!file_exists('./.phpmd.xml')) { copy('./.phpmd.dist.xml', './.phpmd.xml'); }\"",
|
"@php -r \"if (!file_exists('./.phpmd.xml')) { copy('./.phpmd.dist.xml', './.phpmd.xml'); }\"",
|
||||||
"@php vendor/bin/phpmd ./bin,./public,./src ansi .phpmd.xml --cache"
|
"@php vendor/bin/phpmd bin/,public/,src/ ansi .phpmd.xml --cache"
|
||||||
],
|
],
|
||||||
"phpstan:check": [
|
"phpstan:check": [
|
||||||
"@php vendor/bin/phpstan"
|
"@php vendor/bin/phpstan"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,11 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<ruleset name="OCC Standard Ruleset">
|
<ruleset name="OCC Standard Ruleset">
|
||||||
<description>Open Culture Consulting strictly follows PSR standards.</description>
|
<description>Open Culture Consulting strictly follows PSR standards.</description>
|
||||||
|
<file>./bin</file>
|
||||||
|
<file>./public</file>
|
||||||
<file>./src</file>
|
<file>./src</file>
|
||||||
<arg name="extensions" value="php"/>
|
<arg name="extensions" value="php"/>
|
||||||
<rule ref="PSR12">
|
<rule ref="PSR12">
|
||||||
<exclude name="Generic.Files.LineLength"/>
|
<exclude name="Generic.Files.LineLength"/>
|
||||||
<exclude name="PSR2.Classes.PropertyDeclaration.Underscore"/>
|
|
||||||
<exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
|
|
||||||
</rule>
|
</rule>
|
||||||
</ruleset>
|
</ruleset>
|
||||||
|
|
|
@ -5,11 +5,14 @@
|
||||||
|
|
||||||
includes:
|
includes:
|
||||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
|
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||||
|
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
level: 9
|
level: 9
|
||||||
strictRules:
|
treatPhpDocTypesAsCertain: false
|
||||||
noVariableVariables: false
|
ignoreErrors:
|
||||||
|
- identifier: ternary.shortNotAllowed
|
||||||
paths:
|
paths:
|
||||||
- bin
|
- bin
|
||||||
- public
|
- public
|
||||||
|
|
|
@ -8,23 +8,40 @@
|
||||||
findUnusedBaselineEntry="true"
|
findUnusedBaselineEntry="true"
|
||||||
findUnusedCode="true"
|
findUnusedCode="true"
|
||||||
findUnusedVariablesAndParams="true"
|
findUnusedVariablesAndParams="true"
|
||||||
|
reportMixedIssues="false"
|
||||||
>
|
>
|
||||||
<issueHandlers>
|
<issueHandlers>
|
||||||
<!--
|
<!--
|
||||||
Psalm doesn't recognize $columns['idColumn'] and $columns['contentColumn'] always being set in execute().
|
Psalm doesn't recognize some variables always being set because of prior validation.
|
||||||
-->
|
-->
|
||||||
<PossiblyNullArrayOffset>
|
<PossiblyNullReference>
|
||||||
<errorLevel type="suppress">
|
<errorLevel type="suppress">
|
||||||
<file name="src/Console/CsvImportCommand.php"/>
|
<file name="src/Console/UpdateFormatsCommand.php"/>
|
||||||
</errorLevel>
|
</errorLevel>
|
||||||
</PossiblyNullArrayOffset>
|
</PossiblyNullReference>
|
||||||
|
<!--
|
||||||
|
DBAL entities require getter/setter methods even if they are never called directly.
|
||||||
|
-->
|
||||||
<PossiblyUnusedMethod errorLevel="suppress"/>
|
<PossiblyUnusedMethod errorLevel="suppress"/>
|
||||||
<PossiblyUnusedReturnValue errorLevel="suppress"/>
|
<PossiblyUnusedReturnValue errorLevel="suppress"/>
|
||||||
|
<!--
|
||||||
|
Some properties are not set in the constructor and hence checked for initialization.
|
||||||
|
-->
|
||||||
<PropertyNotSetInConstructor errorLevel="suppress"/>
|
<PropertyNotSetInConstructor errorLevel="suppress"/>
|
||||||
<RedundantCastGivenDocblockType errorLevel="suppress"/>
|
|
||||||
<RedundantConditionGivenDocblockType errorLevel="suppress"/>
|
|
||||||
<RedundantFunctionCallGivenDocblockType errorLevel="suppress"/>
|
|
||||||
<RedundantPropertyInitializationCheck errorLevel="suppress"/>
|
<RedundantPropertyInitializationCheck errorLevel="suppress"/>
|
||||||
|
<!--
|
||||||
|
We deliberately want to evaluate empty strings as FALSE in those files.
|
||||||
|
-->
|
||||||
|
<RiskyTruthyFalsyComparison>
|
||||||
|
<errorLevel type="suppress">
|
||||||
|
<file name="src/Console/AddRecordCommand.php"/>
|
||||||
|
<file name="src/Console/AddSetCommand.php"/>
|
||||||
|
</errorLevel>
|
||||||
|
</RiskyTruthyFalsyComparison>
|
||||||
|
<!--
|
||||||
|
Those classes are dynamically used depending on the given OAI verb.
|
||||||
|
@see src/Middleware/Dispatcher.php:95
|
||||||
|
-->
|
||||||
<UnusedClass>
|
<UnusedClass>
|
||||||
<errorLevel type="suppress">
|
<errorLevel type="suppress">
|
||||||
<referencedClass name="OCC\OaiPmh2\Middleware\GetRecord"/>
|
<referencedClass name="OCC\OaiPmh2\Middleware\GetRecord"/>
|
||||||
|
|
|
@ -31,19 +31,19 @@ use OCC\PSR15\QueueRequestHandler;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*/
|
*/
|
||||||
class App
|
final class App
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The PSR-15 Server Request Handler.
|
* The PSR-15 Server Request Handler.
|
||||||
*/
|
*/
|
||||||
protected QueueRequestHandler $requestHandler;
|
private QueueRequestHandler $requestHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate application.
|
* Instantiate application.
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->requestHandler = new QueueRequestHandler([new Dispatcher()]);
|
$this->requestHandler = new QueueRequestHandler(middlewares: [new Dispatcher()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,6 +54,9 @@ class App
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->requestHandler->handle();
|
$this->requestHandler->handle();
|
||||||
|
if ($this->requestHandler->response->hasHeader('Warning')) {
|
||||||
|
// An exception occured. Maybe we don't want to output the response, but log an error instead?
|
||||||
|
}
|
||||||
$this->requestHandler->respond();
|
$this->requestHandler->respond();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,10 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2;
|
namespace OCC\OaiPmh2;
|
||||||
|
|
||||||
use OCC\Basics\Traits\Singleton;
|
use OCC\Basics\Traits\Singleton;
|
||||||
|
use OCC\OaiPmh2\Validator\ConfigurationValidator;
|
||||||
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
|
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
|
||||||
use Symfony\Component\Filesystem\Path;
|
use Symfony\Component\Filesystem\Path;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
use Symfony\Component\Validator\Validation;
|
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,18 +35,18 @@ use Symfony\Component\Yaml\Yaml;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*
|
*
|
||||||
* @property-read string $repositoryName
|
* @property-read string $repositoryName Common name of this repository
|
||||||
* @property-read string $adminEmail
|
* @property-read string $adminEmail Repository contact's e-mail address
|
||||||
* @property-read string $database
|
* @property-read string $database Database's data source name (DSN)
|
||||||
* @property-read array $metadataPrefix
|
* @property-read array $metadataPrefix Array of served metadata prefixes
|
||||||
* @property-read string $deletedRecords
|
* @property-read string $deletedRecords Repository's deleted records policy
|
||||||
* @property-read int $maxRecords
|
* @property-read int $maxRecords Maximum number of records served per request
|
||||||
* @property-read int $tokenValid
|
* @property-read int $tokenValid Number of seconds resumption tokens are valid
|
||||||
*
|
*
|
||||||
* @template TKey of string
|
* @template TKey of string
|
||||||
* @template TValue of array|int|string
|
* @template TValue of array|int|string
|
||||||
*/
|
*/
|
||||||
class Configuration
|
final class Configuration
|
||||||
{
|
{
|
||||||
use Singleton;
|
use Singleton;
|
||||||
|
|
||||||
|
@ -65,98 +64,6 @@ class Configuration
|
||||||
*/
|
*/
|
||||||
protected readonly array $settings;
|
protected readonly array $settings;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get constraints for configuration array.
|
|
||||||
*
|
|
||||||
* @return Assert\Collection The collection of constraints
|
|
||||||
*/
|
|
||||||
protected function getValidationConstraints(): Assert\Collection
|
|
||||||
{
|
|
||||||
return new Assert\Collection([
|
|
||||||
'repositoryName' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
],
|
|
||||||
'adminEmail' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\Email(['mode' => 'html5']),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
],
|
|
||||||
'database' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
],
|
|
||||||
'metadataPrefix' => [
|
|
||||||
new Assert\Type('array'),
|
|
||||||
new Assert\All([
|
|
||||||
new Assert\Collection([
|
|
||||||
'schema' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\Url(),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
],
|
|
||||||
'namespace' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\Url(),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
]
|
|
||||||
])
|
|
||||||
])
|
|
||||||
],
|
|
||||||
'deletedRecords' => [
|
|
||||||
new Assert\Type('string'),
|
|
||||||
new Assert\Choice(['no', 'persistent', 'transient']),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
],
|
|
||||||
'maxRecords' => [
|
|
||||||
new Assert\Type('int'),
|
|
||||||
new Assert\Range([
|
|
||||||
'min' => 1,
|
|
||||||
'max' => 100
|
|
||||||
])
|
|
||||||
],
|
|
||||||
'tokenValid' => [
|
|
||||||
new Assert\Type('int'),
|
|
||||||
new Assert\Range([
|
|
||||||
'min' => 300,
|
|
||||||
'max' => 86400
|
|
||||||
])
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read and validate configuration file.
|
|
||||||
*
|
|
||||||
* @return array<TKey, TValue> The configuration array
|
|
||||||
*
|
|
||||||
* @throws FileNotFoundException if configuration file does not exist
|
|
||||||
* @throws ValidationFailedException if configuration file is not valid
|
|
||||||
*/
|
|
||||||
protected function loadConfigFile(): array
|
|
||||||
{
|
|
||||||
$configPath = Path::canonicalize(self::CONFIG_FILE);
|
|
||||||
if (!is_readable($configPath)) {
|
|
||||||
throw new FileNotFoundException(
|
|
||||||
sprintf(
|
|
||||||
'Configuration file "%s" not found or not readable.',
|
|
||||||
$configPath
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
null,
|
|
||||||
$configPath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/** @var array<TKey, TValue> */
|
|
||||||
$config = Yaml::parseFile($configPath);
|
|
||||||
$validator = Validation::createValidator();
|
|
||||||
$violations = $validator->validate($config, $this->getValidationConstraints());
|
|
||||||
if ($violations->count() > 0) {
|
|
||||||
throw new ValidationFailedException(null, $violations);
|
|
||||||
}
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load and validate configuration settings from YAML file.
|
* Load and validate configuration settings from YAML file.
|
||||||
*
|
*
|
||||||
|
@ -165,11 +72,24 @@ class Configuration
|
||||||
*/
|
*/
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
try {
|
$configPath = Path::canonicalize(path: self::CONFIG_FILE);
|
||||||
$this->settings = $this->loadConfigFile();
|
if (!is_readable(filename: $configPath)) {
|
||||||
} catch (FileNotFoundException | ValidationFailedException $exception) {
|
throw new FileNotFoundException(
|
||||||
throw $exception;
|
message: 'Configuration file not found or not readable.',
|
||||||
|
code: 500,
|
||||||
|
path: $configPath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
/** @var array<TKey, TValue> */
|
||||||
|
$config = Yaml::parseFile(filename: $configPath);
|
||||||
|
$violations = ConfigurationValidator::validate(config: $config);
|
||||||
|
if ($violations->count() > 0) {
|
||||||
|
throw new ValidationFailedException(
|
||||||
|
value: null,
|
||||||
|
violations: $violations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->settings = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -177,7 +97,7 @@ class Configuration
|
||||||
*
|
*
|
||||||
* @param TKey $name The setting to retrieve
|
* @param TKey $name The setting to retrieve
|
||||||
*
|
*
|
||||||
* @return TValue|null The setting or NULL
|
* @return ?TValue The setting or NULL
|
||||||
*/
|
*/
|
||||||
public function __get(string $name): mixed
|
public function __get(string $name): mixed
|
||||||
{
|
{
|
||||||
|
|
121
src/Console.php
121
src/Console.php
|
@ -34,9 +34,53 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @psalm-type CliArguments = array{
|
||||||
|
* identifier: string,
|
||||||
|
* format: string,
|
||||||
|
* file: string,
|
||||||
|
* sets?: list<string>,
|
||||||
|
* setSpec: string,
|
||||||
|
* setName: string,
|
||||||
|
* idColumn: string,
|
||||||
|
* contentColumn: string,
|
||||||
|
* dateColumn: string,
|
||||||
|
* setColumn: string,
|
||||||
|
* noValidation: bool,
|
||||||
|
* force: bool
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
abstract class Console extends Command
|
abstract class Console extends Command
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* This holds the command's arguments and options.
|
||||||
|
*
|
||||||
|
* @var CliArguments
|
||||||
|
*/
|
||||||
|
protected array $arguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the entity manager singleton.
|
||||||
|
*/
|
||||||
|
protected EntityManager $em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the PHP memory limit in bytes.
|
||||||
|
*/
|
||||||
|
protected int $memoryLimit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes changes to the database if memory limit reaches 50%.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function checkMemoryUsage(): void
|
||||||
|
{
|
||||||
|
if ((memory_get_usage() / $this->getPhpMemoryLimit()) > 0.5) {
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the result cache.
|
* Clears the result cache.
|
||||||
*
|
*
|
||||||
|
@ -47,11 +91,13 @@ abstract class Console extends Command
|
||||||
/** @var Application */
|
/** @var Application */
|
||||||
$app = $this->getApplication();
|
$app = $this->getApplication();
|
||||||
$app->doRun(
|
$app->doRun(
|
||||||
new ArrayInput([
|
input: new ArrayInput(
|
||||||
|
parameters: [
|
||||||
'command' => 'orm:clear-cache:result',
|
'command' => 'orm:clear-cache:result',
|
||||||
'--flush' => true
|
'--flush' => true
|
||||||
]),
|
]
|
||||||
new NullOutput()
|
),
|
||||||
|
output: new NullOutput()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +108,8 @@ abstract class Console extends Command
|
||||||
*/
|
*/
|
||||||
protected function getPhpMemoryLimit(): int
|
protected function getPhpMemoryLimit(): int
|
||||||
{
|
{
|
||||||
$ini = trim(ini_get('memory_limit'));
|
if (!isset($this->memoryLimit)) {
|
||||||
|
$ini = trim(string: ini_get(option: 'memory_limit'));
|
||||||
$limit = (int) $ini;
|
$limit = (int) $ini;
|
||||||
if ($limit < 0) {
|
if ($limit < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -78,7 +125,9 @@ abstract class Console extends Command
|
||||||
case 'k':
|
case 'k':
|
||||||
$limit *= 1024;
|
$limit *= 1024;
|
||||||
}
|
}
|
||||||
return $limit;
|
$this->memoryLimit = $limit;
|
||||||
|
}
|
||||||
|
return $this->memoryLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,32 +140,68 @@ abstract class Console extends Command
|
||||||
*/
|
*/
|
||||||
protected function validateInput(InputInterface $input, OutputInterface $output): bool
|
protected function validateInput(InputInterface $input, OutputInterface $output): bool
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
/** @var CliArguments */
|
||||||
$arguments = $input->getArguments();
|
$mergedArguments = array_merge($input->getArguments(), $input->getOptions());
|
||||||
|
$this->arguments = $mergedArguments;
|
||||||
|
|
||||||
$formats = Database::getInstance()->getMetadataFormats()->getQueryResult();
|
if (array_key_exists('format', $this->arguments)) {
|
||||||
if (!array_key_exists($arguments['format'], $formats)) {
|
$formats = $this->em->getMetadataFormats();
|
||||||
$output->writeln([
|
if (!$formats->containsKey(key: $this->arguments['format'])) {
|
||||||
|
$output->writeln(
|
||||||
|
messages: [
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] Metadata format "%s" is not supported. ',
|
format: ' [ERROR] Metadata format "%s" is not supported. ',
|
||||||
$arguments['format']
|
values: $this->arguments['format']
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!is_readable($arguments['file'])) {
|
}
|
||||||
$output->writeln([
|
if (array_key_exists('file', $this->arguments) && !is_readable(filename: $this->arguments['file'])) {
|
||||||
|
$output->writeln(
|
||||||
|
messages: [
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] File "%s" not found or not readable. ',
|
format: ' [ERROR] File "%s" not found or not readable. ',
|
||||||
$arguments['file']
|
values: $this->arguments['file']
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('sets', $this->arguments)) {
|
||||||
|
$sets = $this->em->getSets();
|
||||||
|
$invalidSets = array_diff($this->arguments['sets'], $sets->getKeys());
|
||||||
|
if (count($invalidSets) !== 0) {
|
||||||
|
$output->writeln(
|
||||||
|
messages: [
|
||||||
|
'',
|
||||||
|
sprintf(
|
||||||
|
format: ' [ERROR] Sets "%s" are not supported. ',
|
||||||
|
values: implode('", "', $invalidSets)
|
||||||
|
),
|
||||||
|
''
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new console command instance.
|
||||||
|
*
|
||||||
|
* @param ?string $name The name of the command
|
||||||
|
* passing null means it must be set in configure()
|
||||||
|
*/
|
||||||
|
public function __construct(?string $name = null)
|
||||||
|
{
|
||||||
|
$this->em = EntityManager::getInstance();
|
||||||
|
parent::__construct($name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2\Console;
|
namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
use OCC\OaiPmh2\Entity\Format;
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
use OCC\OaiPmh2\Entity\Record;
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
use OCC\OaiPmh2\Entity\Set;
|
||||||
|
@ -70,7 +69,7 @@ class AddRecordCommand extends Console
|
||||||
$this->addArgument(
|
$this->addArgument(
|
||||||
'sets',
|
'sets',
|
||||||
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
|
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
|
||||||
'The list of sets to associate the record with.'
|
'Optional: The list of sets to associate the record with.'
|
||||||
);
|
);
|
||||||
parent::configure();
|
parent::configure();
|
||||||
}
|
}
|
||||||
|
@ -85,36 +84,31 @@ class AddRecordCommand extends Console
|
||||||
*/
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
if (!$this->validateInput($input, $output)) {
|
if (!$this->validateInput(input: $input, output: $output)) {
|
||||||
return Command::INVALID;
|
return Command::INVALID;
|
||||||
}
|
}
|
||||||
/** @var string */
|
|
||||||
$identifier = $input->getArgument('identifier');
|
|
||||||
/** @var Format */
|
/** @var Format */
|
||||||
$format = Database::getInstance()
|
$format = $this->em->getMetadataFormat(prefix: $this->arguments['format']);
|
||||||
->getEntityManager()
|
$content = file_get_contents(filename: $this->arguments['file']) ?: '';
|
||||||
->getReference(Format::class, $input->getArgument('format'));
|
|
||||||
/** @var string */
|
|
||||||
$file = $input->getArgument('file');
|
|
||||||
/** @var string[] */
|
|
||||||
$sets = $input->getArgument('sets');
|
|
||||||
/** @var string */
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
|
|
||||||
$record = new Record($identifier, $format);
|
$record = new Record(
|
||||||
|
identifier: $this->arguments['identifier'],
|
||||||
|
format: $format
|
||||||
|
);
|
||||||
if (trim($content) !== '') {
|
if (trim($content) !== '') {
|
||||||
$record->setContent($content);
|
$record->setContent(data: $content);
|
||||||
}
|
}
|
||||||
foreach ($sets as $set) {
|
if (array_key_exists('sets', $this->arguments)) {
|
||||||
|
foreach ($this->arguments['sets'] as $set) {
|
||||||
/** @var Set */
|
/** @var Set */
|
||||||
$setSpec = Database::getInstance()
|
$setSpec = $this->em->getSet(spec: $set);
|
||||||
->getEntityManager()
|
$record->addSet(set: $setSpec);
|
||||||
->getReference(Set::class, $set);
|
}
|
||||||
$record->addSet($setSpec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::getInstance()->addOrUpdateRecord($record);
|
$this->em->addOrUpdate(entity: $record);
|
||||||
Database::getInstance()->pruneOrphanSets();
|
$this->em->pruneOrphanedSets();
|
||||||
|
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
|
|
||||||
|
@ -122,7 +116,7 @@ class AddRecordCommand extends Console
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] Record "%s" with metadata prefix "%s" added or updated successfully! ',
|
' [OK] Record "%s" with metadata prefix "%s" added or updated successfully! ',
|
||||||
$identifier,
|
$this->arguments['identifier'],
|
||||||
$format->getPrefix()
|
$format->getPrefix()
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
|
|
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2\Console;
|
namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
use OCC\OaiPmh2\Entity\Set;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
@ -53,10 +52,10 @@ class AddSetCommand extends Console
|
||||||
$this->addArgument(
|
$this->addArgument(
|
||||||
'setSpec',
|
'setSpec',
|
||||||
InputArgument::REQUIRED,
|
InputArgument::REQUIRED,
|
||||||
'The set (spec) to update.',
|
'The set (spec) to add or update.',
|
||||||
null,
|
null,
|
||||||
function (): array {
|
function (): array {
|
||||||
return array_keys(Database::getInstance()->getAllSets()->getQueryResult());
|
return $this->em->getSets()->getKeys();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
$this->addArgument(
|
$this->addArgument(
|
||||||
|
@ -67,7 +66,7 @@ class AddSetCommand extends Console
|
||||||
$this->addArgument(
|
$this->addArgument(
|
||||||
'file',
|
'file',
|
||||||
InputArgument::OPTIONAL,
|
InputArgument::OPTIONAL,
|
||||||
'The optional file containing the set description XML.'
|
'Optional: The file containing the set description XML.'
|
||||||
);
|
);
|
||||||
parent::configure();
|
parent::configure();
|
||||||
}
|
}
|
||||||
|
@ -82,32 +81,20 @@ class AddSetCommand extends Console
|
||||||
*/
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
if (!$this->validateInput(input: $input, output: $output)) {
|
||||||
$arguments = $input->getArguments();
|
|
||||||
$description = null;
|
|
||||||
|
|
||||||
if (isset($arguments['file'])) {
|
|
||||||
if (!is_readable($arguments['file'])) {
|
|
||||||
$output->writeln([
|
|
||||||
'',
|
|
||||||
sprintf(
|
|
||||||
' [ERROR] File "%s" not found or not readable. ',
|
|
||||||
$arguments['file']
|
|
||||||
),
|
|
||||||
''
|
|
||||||
]);
|
|
||||||
return Command::INVALID;
|
return Command::INVALID;
|
||||||
} else {
|
|
||||||
$description = (string) file_get_contents($arguments['file']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('file', $this->arguments)) {
|
||||||
|
$description = file_get_contents(filename: $this->arguments['file']) ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$set = new Set(
|
$set = new Set(
|
||||||
$arguments['setSpec'],
|
spec: $this->arguments['setSpec'],
|
||||||
$arguments['setName'],
|
name: $this->arguments['setName'],
|
||||||
$description
|
description: $description ?? null
|
||||||
);
|
);
|
||||||
Database::getInstance()->addOrUpdateSet($set);
|
$this->em->addOrUpdate(entity: $set);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,7 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2\Console;
|
namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use OCC\OaiPmh2\Configuration;
|
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
use OCC\OaiPmh2\Entity\Format;
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
use OCC\OaiPmh2\Entity\Record;
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
use OCC\OaiPmh2\Entity\Set;
|
||||||
|
@ -42,6 +40,13 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @psalm-type ColumnMapping = array{
|
||||||
|
* idColumn: int,
|
||||||
|
* contentColumn: int,
|
||||||
|
* dateColumn: ?int,
|
||||||
|
* setColumn: ?int
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'oai:records:import:csv',
|
name: 'oai:records:import:csv',
|
||||||
|
@ -62,7 +67,7 @@ class CsvImportCommand extends Console
|
||||||
'The format (metadata prefix) of the records.',
|
'The format (metadata prefix) of the records.',
|
||||||
null,
|
null,
|
||||||
function (): array {
|
function (): array {
|
||||||
return array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
|
return $this->em->getMetadataFormats()->getKeys();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
$this->addArgument(
|
$this->addArgument(
|
||||||
|
@ -88,21 +93,21 @@ class CsvImportCommand extends Console
|
||||||
'dateColumn',
|
'dateColumn',
|
||||||
'd',
|
'd',
|
||||||
InputOption::VALUE_OPTIONAL,
|
InputOption::VALUE_OPTIONAL,
|
||||||
'Name of the CSV column which holds the records\' datetime of last change.',
|
'Optional: Name of the CSV column which holds the records\' datetime of last change.',
|
||||||
'lastChanged'
|
'lastChanged'
|
||||||
);
|
);
|
||||||
$this->addOption(
|
$this->addOption(
|
||||||
'setColumn',
|
'setColumn',
|
||||||
's',
|
's',
|
||||||
InputOption::VALUE_OPTIONAL,
|
InputOption::VALUE_OPTIONAL,
|
||||||
'Name of the CSV column which holds the comma-separated list of the records\' sets.',
|
'Optional: Name of the CSV column which holds the comma-separated list of the records\' sets.',
|
||||||
'sets'
|
'sets'
|
||||||
);
|
);
|
||||||
$this->addOption(
|
$this->addOption(
|
||||||
'noValidation',
|
'noValidation',
|
||||||
null,
|
null,
|
||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
'Skip content validation (improves performance for large record sets).'
|
'Optional: Skip content validation (improves ingest performance for large record sets).'
|
||||||
);
|
);
|
||||||
parent::configure();
|
parent::configure();
|
||||||
}
|
}
|
||||||
|
@ -117,69 +122,60 @@ class CsvImportCommand extends Console
|
||||||
*/
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
if (!$this->validateInput($input, $output)) {
|
if (!$this->validateInput(input: $input, output: $output)) {
|
||||||
return Command::INVALID;
|
return Command::INVALID;
|
||||||
}
|
}
|
||||||
$phpMemoryLimit = $this->getPhpMemoryLimit();
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
|
||||||
$arguments = $input->getArguments();
|
|
||||||
/** @var bool */
|
|
||||||
$noValidation = $input->getOption('noValidation');
|
|
||||||
/** @var resource */
|
/** @var resource */
|
||||||
$file = fopen($arguments['file'], 'r');
|
$file = fopen(filename: $this->arguments['file'], mode: 'r');
|
||||||
|
|
||||||
$columns = $this->getColumnNames($input, $output, $file);
|
$columnMapping = $this->getColumnNames(input: $input, output: $output, file: $file);
|
||||||
if (count($columns) === 0) {
|
|
||||||
return Command::INVALID;
|
if (!isset($columnMapping)) {
|
||||||
|
return Command::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$progressIndicator = new ProgressIndicator($output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']);
|
$progressIndicator = new ProgressIndicator($output, null, 100, ['⠏', '⠛', '⠹', '⢸', '⣰', '⣤', '⣆', '⡇']);
|
||||||
$progressIndicator->start('Importing...');
|
$progressIndicator->start('Importing...');
|
||||||
|
|
||||||
while ($row = fgetcsv($file)) {
|
while ($row = fgetcsv(stream: $file)) {
|
||||||
/** @var Format */
|
/** @var Format */
|
||||||
$format = Database::getInstance()
|
$format = $this->em->getMetadataFormat(prefix: $this->arguments['format']);
|
||||||
->getEntityManager()
|
$record = new Record(
|
||||||
->getReference(Format::class, $arguments['format']);
|
identifier: $row[$columnMapping['idColumn']],
|
||||||
$record = new Record($row[$columns['idColumn']], $format);
|
format: $format
|
||||||
if (strlen(trim($row[$columns['contentColumn']])) > 0) {
|
);
|
||||||
$record->setContent($row[$columns['contentColumn']], !$noValidation);
|
if (strlen(trim($row[$columnMapping['contentColumn']])) > 0) {
|
||||||
|
$record->setContent(
|
||||||
|
data: $row[$columnMapping['contentColumn']],
|
||||||
|
validate: !$this->arguments['noValidation']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (isset($columns['dateColumn'])) {
|
if (isset($columnMapping['dateColumn'])) {
|
||||||
$record->setLastChanged(new DateTime($row[$columns['dateColumn']]));
|
$record->setLastChanged(dateTime: new DateTime($row[$columnMapping['dateColumn']]));
|
||||||
}
|
}
|
||||||
if (isset($columns['setColumn'])) {
|
if (isset($columnMapping['setColumn'])) {
|
||||||
$sets = $row[$columns['setColumn']];
|
$sets = $row[$columnMapping['setColumn']];
|
||||||
foreach (explode(',', $sets) as $set) {
|
foreach (explode(',', $sets) as $set) {
|
||||||
/** @var Set */
|
/** @var Set */
|
||||||
$setSpec = Database::getInstance()
|
$setSpec = $this->em->getSet(spec: trim($set));
|
||||||
->getEntityManager()
|
$record->addSet(set: $setSpec);
|
||||||
->getReference(Set::class, trim($set));
|
|
||||||
$record->addSet($setSpec);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Database::getInstance()->addOrUpdateRecord($record, true);
|
$this->em->addOrUpdate(entity: $record, bulkMode: true);
|
||||||
|
|
||||||
++$count;
|
++$count;
|
||||||
$progressIndicator->advance();
|
$progressIndicator->advance();
|
||||||
$progressIndicator->setMessage('Importing... ' . (string) $count . ' records processed.');
|
$progressIndicator->setMessage('Importing... ' . (string) $count . ' records processed.');
|
||||||
|
$this->checkMemoryUsage();
|
||||||
// Flush to database if memory usage reaches 50% or every 10.000 records.
|
|
||||||
if ((memory_get_usage() / $phpMemoryLimit) > 0.5 || ($count % 10000) === 0) {
|
|
||||||
$progressIndicator->setMessage(
|
|
||||||
'Importing... ' . (string) $count . ' records processed. Flushing to database...'
|
|
||||||
);
|
|
||||||
Database::getInstance()->flush(true);
|
|
||||||
}
|
}
|
||||||
}
|
$this->em->flush();
|
||||||
Database::getInstance()->flush(true);
|
$this->em->pruneOrphanedSets();
|
||||||
Database::getInstance()->pruneOrphanSets();
|
|
||||||
|
|
||||||
$progressIndicator->finish('All done!');
|
$progressIndicator->finish('All done!');
|
||||||
|
|
||||||
fclose($file);
|
fclose(stream: $file);
|
||||||
|
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
|
|
||||||
|
@ -188,7 +184,7 @@ class CsvImportCommand extends Console
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] %d records with metadata prefix "%s" were imported successfully! ',
|
' [OK] %d records with metadata prefix "%s" were imported successfully! ',
|
||||||
$count,
|
$count,
|
||||||
$arguments['format']
|
$this->arguments['format']
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
|
@ -196,49 +192,57 @@ class CsvImportCommand extends Console
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the column names of CSV.
|
* Get the column numbers of CSV.
|
||||||
*
|
*
|
||||||
* @param InputInterface $input The inputs
|
* @param InputInterface $input The input
|
||||||
* @param OutputInterface $output The output interface
|
* @param OutputInterface $output The output
|
||||||
* @param resource $file The handle for the CSV file
|
* @param resource $file The handle for the CSV file
|
||||||
*
|
*
|
||||||
* @return array<string, int|string|null> The mapped column names
|
* @return ?ColumnMapping The mapped columns or NULL in case of an error
|
||||||
*/
|
*/
|
||||||
protected function getColumnNames(InputInterface $input, OutputInterface $output, $file): array
|
protected function getColumnNames(InputInterface $input, OutputInterface $output, $file): ?array
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
/** @var array{idColumn: string, contentColumn: string, dateColumn: string, setColumn: string} */
|
||||||
$options = $input->getOptions();
|
$columns = [
|
||||||
|
'idColumn' => $input->getOption('idColumn'),
|
||||||
$columns = [];
|
'contentColumn' => $input->getOption('contentColumn'),
|
||||||
|
'dateColumn' => $input->getOption('dateColumn'),
|
||||||
|
'setColumn' => $input->getOption('setColumn')
|
||||||
|
];
|
||||||
|
|
||||||
$headers = fgetcsv($file);
|
$headers = fgetcsv($file);
|
||||||
if (!is_array($headers)) {
|
if (!is_array($headers) || !isset($headers[0])) {
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] File "%s" does not contain valid CSV. ',
|
format: ' [ERROR] File "%s" does not contain valid CSV. ',
|
||||||
stream_get_meta_data($file)['uri'] ?? 'unknown'
|
/** @phpstan-ignore-next-line - URI is always set for fopen() resources. */
|
||||||
|
values: stream_get_meta_data(stream: $file)['uri'] ?: 'unknown'
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
return [];
|
return null;
|
||||||
} else {
|
}
|
||||||
|
/** @var array<string, int> */
|
||||||
$headers = array_flip($headers);
|
$headers = array_flip($headers);
|
||||||
}
|
|
||||||
foreach ($options as $option => $value) {
|
$callback = function (string $column) use ($headers): ?int {
|
||||||
$columns[$option] = $headers[$value] ?? null;
|
return array_key_exists($column, $headers) ? $headers[$column] : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
$columns = array_map($callback, $columns);
|
||||||
|
|
||||||
if (!isset($columns['idColumn']) || !isset($columns['contentColumn'])) {
|
if (!isset($columns['idColumn']) || !isset($columns['contentColumn'])) {
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] File "%s" does not contain valid CSV. ',
|
format: ' [ERROR] File "%s" does not contain mandatory columns. ',
|
||||||
stream_get_meta_data($file)['uri'] ?? 'unknown'
|
/** @phpstan-ignore-next-line - URI is always set for fopen() resources. */
|
||||||
|
values: stream_get_meta_data($file)['uri'] ?: 'unknown'
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
return $columns;
|
return $columns;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,6 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2\Console;
|
namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
@ -40,7 +37,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||||
*/
|
*/
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'oai:records:delete',
|
name: 'oai:records:delete',
|
||||||
description: 'Delete a record from database'
|
description: 'Delete a record while obeying deleted record policy'
|
||||||
)]
|
)]
|
||||||
class DeleteRecordCommand extends Console
|
class DeleteRecordCommand extends Console
|
||||||
{
|
{
|
||||||
|
@ -74,28 +71,24 @@ class DeleteRecordCommand extends Console
|
||||||
*/
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
if (!$this->validateInput(input: $input, output: $output)) {
|
||||||
$arguments = $input->getArguments();
|
return Command::INVALID;
|
||||||
$entityManager = Database::getInstance()->getEntityManager();
|
}
|
||||||
|
|
||||||
$format = $entityManager->getReference(Format::class, $arguments['format']);
|
$record = $this->em->getRecord(
|
||||||
$record = $entityManager->find(
|
identifier: $this->arguments['identifier'],
|
||||||
Record::class,
|
format: $this->arguments['format']
|
||||||
[
|
|
||||||
'identifier' => $arguments['identifier'],
|
|
||||||
'format' => $format
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isset($record)) {
|
if (isset($record)) {
|
||||||
Database::getInstance()->deleteRecord($record);
|
$this->em->delete(entity: $record);
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] Record "%s" with metadata prefix "%s" successfully deleted. ',
|
' [OK] Record "%s" with metadata prefix "%s" successfully (marked as) deleted. ',
|
||||||
$arguments['identifier'],
|
$this->arguments['identifier'],
|
||||||
$arguments['format']
|
$this->arguments['format']
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
|
@ -105,8 +98,8 @@ class DeleteRecordCommand extends Console
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] Record "%s" with metadata prefix "%s" not found. ',
|
' [ERROR] Record "%s" with metadata prefix "%s" not found. ',
|
||||||
$arguments['identifier'],
|
$this->arguments['identifier'],
|
||||||
$arguments['format']
|
$this->arguments['format']
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -24,7 +24,6 @@ namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Configuration;
|
use OCC\OaiPmh2\Configuration;
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
@ -54,7 +53,7 @@ class PruneDeletedRecordsCommand extends Console
|
||||||
'force',
|
'force',
|
||||||
'f',
|
'f',
|
||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
'Deletes records even under "transient" policy.'
|
'Optional: Deletes records even under "transient" policy.'
|
||||||
);
|
);
|
||||||
parent::configure();
|
parent::configure();
|
||||||
}
|
}
|
||||||
|
@ -75,13 +74,13 @@ class PruneDeletedRecordsCommand extends Console
|
||||||
$policy === 'no'
|
$policy === 'no'
|
||||||
or ($policy === 'transient' && $forced)
|
or ($policy === 'transient' && $forced)
|
||||||
) {
|
) {
|
||||||
$deleted = Database::getInstance()->pruneDeletedRecords();
|
$deleted = $this->em->pruneDeletedRecords();
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] %d records are deleted and were successfully removed! ',
|
format: ' [OK] %d deleted records were successfully removed! ',
|
||||||
$deleted
|
values: $deleted
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||||
namespace OCC\OaiPmh2\Console;
|
namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
@ -51,12 +50,12 @@ class PruneResumptionTokensCommand extends Console
|
||||||
*/
|
*/
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$expired = Database::getInstance()->pruneResumptionTokens();
|
$expired = $this->em->pruneExpiredTokens();
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
'',
|
'',
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] %d resumption tokens are expired and were successfully deleted! ',
|
format: ' [OK] %d expired resumption tokens were successfully deleted! ',
|
||||||
$expired
|
values: $expired
|
||||||
),
|
),
|
||||||
''
|
''
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -24,7 +24,6 @@ namespace OCC\OaiPmh2\Console;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Configuration;
|
use OCC\OaiPmh2\Configuration;
|
||||||
use OCC\OaiPmh2\Console;
|
use OCC\OaiPmh2\Console;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
use OCC\OaiPmh2\Entity\Format;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
@ -57,56 +56,55 @@ class UpdateFormatsCommand extends Console
|
||||||
/** @var array<string, array<string, string>> */
|
/** @var array<string, array<string, string>> */
|
||||||
$formats = Configuration::getInstance()->metadataPrefix;
|
$formats = Configuration::getInstance()->metadataPrefix;
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
$inDatabase = Database::getInstance()
|
$inDatabase = $this->em->getMetadataFormats();
|
||||||
->getMetadataFormats()
|
|
||||||
->getQueryResult();
|
|
||||||
$added = 0;
|
|
||||||
$deleted = 0;
|
|
||||||
$failure = false;
|
$failure = false;
|
||||||
foreach ($formats as $prefix => $format) {
|
foreach ($formats as $prefix => $format) {
|
||||||
if (array_key_exists($prefix, $inDatabase)) {
|
|
||||||
if (
|
if (
|
||||||
$format['namespace'] === $inDatabase[$prefix]->getNamespace()
|
$inDatabase->containsKey(key: $prefix)
|
||||||
|
/** @phpstan-ignore-next-line - $inDatabase[$prefix] is always of type Format. */
|
||||||
|
and $format['namespace'] === $inDatabase[$prefix]->getNamespace()
|
||||||
|
/** @phpstan-ignore-next-line - $inDatabase[$prefix] is always of type Format. */
|
||||||
and $format['schema'] === $inDatabase[$prefix]->getSchema()
|
and $format['schema'] === $inDatabase[$prefix]->getSchema()
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$format = new Format($prefix, $format['namespace'], $format['schema']);
|
$format = new Format(
|
||||||
Database::getInstance()->addOrUpdateMetadataFormat($format);
|
prefix: $prefix,
|
||||||
++$added;
|
namespace: $format['namespace'],
|
||||||
|
schema: $format['schema']
|
||||||
|
);
|
||||||
|
$this->em->addOrUpdate(entity: $format);
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] Metadata format "%s" added or updated successfully! ',
|
format: ' [OK] Metadata format "%s" added or updated successfully! ',
|
||||||
$prefix
|
values: $prefix
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
$failure = true;
|
$failure = true;
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
sprintf(
|
sprintf(
|
||||||
' [ERROR] Could not add or update metadata format "%s". ',
|
format: ' [ERROR] Could not add or update metadata format "%s". ',
|
||||||
$prefix
|
values: $prefix
|
||||||
),
|
),
|
||||||
$exception->getMessage()
|
$exception->getMessage()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach (array_keys($inDatabase) as $prefix) {
|
foreach (array_diff($inDatabase->getKeys(), array_keys($formats)) as $prefix) {
|
||||||
if (!array_key_exists($prefix, $formats)) {
|
/** @var Format */
|
||||||
Database::getInstance()->deleteMetadataFormat($inDatabase[$prefix]);
|
$format = $inDatabase[$prefix];
|
||||||
++$deleted;
|
$this->em->delete(entity: $format);
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
sprintf(
|
sprintf(
|
||||||
' [OK] Metadata format "%s" and all associated records deleted successfully! ',
|
format: ' [OK] Metadata format "%s" and all associated records deleted successfully! ',
|
||||||
$prefix
|
values: $prefix
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$this->clearResultCache();
|
$this->clearResultCache();
|
||||||
$currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
|
$currentFormats = $this->em->getMetadataFormats()->getKeys();
|
||||||
if (count($currentFormats) > 0) {
|
if (count($currentFormats) > 0) {
|
||||||
$output->writeln(
|
$output->writeln(
|
||||||
[
|
[
|
||||||
|
@ -118,7 +116,7 @@ class UpdateFormatsCommand extends Console
|
||||||
' command "php bin/cli oai:formats:update" again! ',
|
' command "php bin/cli oai:formats:update" again! ',
|
||||||
''
|
''
|
||||||
],
|
],
|
||||||
1 | 16
|
OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_QUIET
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$output->writeln(
|
$output->writeln(
|
||||||
|
@ -129,13 +127,9 @@ class UpdateFormatsCommand extends Console
|
||||||
' command "php bin/cli oai:formats:update" again! ',
|
' command "php bin/cli oai:formats:update" again! ',
|
||||||
''
|
''
|
||||||
],
|
],
|
||||||
1 | 16
|
OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_QUIET
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!$failure) {
|
return $failure ? Command::FAILURE : Command::SUCCESS;
|
||||||
return Command::SUCCESS;
|
|
||||||
} else {
|
|
||||||
return Command::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
573
src/Database.php
573
src/Database.php
|
@ -1,573 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OAI-PMH 2.0 Data Provider
|
|
||||||
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace OCC\OaiPmh2;
|
|
||||||
|
|
||||||
use DateTime;
|
|
||||||
use Doctrine\Common\Collections\Criteria;
|
|
||||||
use Doctrine\DBAL\DriverManager;
|
|
||||||
use Doctrine\DBAL\Schema\AbstractAsset;
|
|
||||||
use Doctrine\DBAL\Tools\DsnParser;
|
|
||||||
use Doctrine\ORM\AbstractQuery;
|
|
||||||
use Doctrine\ORM\Configuration as DoctrineConfiguration;
|
|
||||||
use Doctrine\ORM\EntityManager;
|
|
||||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
|
||||||
use Doctrine\ORM\Proxy\ProxyFactory;
|
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
|
||||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
|
||||||
use OCC\Basics\Traits\Singleton;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
|
||||||
use OCC\OaiPmh2\Entity\Token;
|
|
||||||
use OCC\OaiPmh2\Result;
|
|
||||||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
|
||||||
use Symfony\Component\Filesystem\Path;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles all database shenanigans.
|
|
||||||
*
|
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
|
||||||
* @package OAIPMH2
|
|
||||||
*
|
|
||||||
* @template Formats of array<string, Format>
|
|
||||||
* @template Records of array<string, Record>
|
|
||||||
* @template Sets of array<string, Set>
|
|
||||||
*/
|
|
||||||
class Database
|
|
||||||
{
|
|
||||||
use Singleton;
|
|
||||||
|
|
||||||
protected const DB_TABLES = [
|
|
||||||
'formats',
|
|
||||||
'records',
|
|
||||||
'records_sets',
|
|
||||||
'sets',
|
|
||||||
'tokens'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This holds the Doctrine entity manager.
|
|
||||||
*/
|
|
||||||
protected EntityManager $entityManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or update metadata format.
|
|
||||||
*
|
|
||||||
* @param Format $newFormat The metadata format
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function addOrUpdateMetadataFormat(Format $newFormat): void
|
|
||||||
{
|
|
||||||
$oldFormat = $this->entityManager->find(Format::class, $newFormat->getPrefix());
|
|
||||||
if (isset($oldFormat)) {
|
|
||||||
$oldFormat->setNamespace($newFormat->getNamespace());
|
|
||||||
$oldFormat->setSchema($newFormat->getSchema());
|
|
||||||
} else {
|
|
||||||
$this->entityManager->persist($newFormat);
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or update record.
|
|
||||||
*
|
|
||||||
* @param Record $newRecord The record
|
|
||||||
* @param bool $bulkMode Should we operate in bulk mode (no flush)?
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function addOrUpdateRecord(Record $newRecord, bool $bulkMode = false): void
|
|
||||||
{
|
|
||||||
$oldRecord = $this->entityManager->find(
|
|
||||||
Record::class,
|
|
||||||
[
|
|
||||||
'identifier' => $newRecord->getIdentifier(),
|
|
||||||
'format' => $newRecord->getFormat()
|
|
||||||
]
|
|
||||||
);
|
|
||||||
if (isset($oldRecord)) {
|
|
||||||
if ($newRecord->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
|
|
||||||
$oldRecord->setContent($newRecord->getContent(), false);
|
|
||||||
$oldRecord->setLastChanged($newRecord->getLastChanged());
|
|
||||||
// Add new sets.
|
|
||||||
foreach (array_diff($newRecord->getSets(), $oldRecord->getSets()) as $newSet) {
|
|
||||||
$oldRecord->addSet($newSet);
|
|
||||||
}
|
|
||||||
// Remove old sets.
|
|
||||||
foreach (array_diff($oldRecord->getSets(), $newRecord->getSets()) as $oldSet) {
|
|
||||||
$oldRecord->removeSet($oldSet);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->entityManager->remove($oldRecord);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($newRecord->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
|
|
||||||
$this->entityManager->persist($newRecord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!$bulkMode) {
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or update set.
|
|
||||||
*
|
|
||||||
* @param Set $newSet The set
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function addOrUpdateSet(Set $newSet): void
|
|
||||||
{
|
|
||||||
$oldSet = $this->entityManager->find(Set::class, $newSet->getSpec());
|
|
||||||
if (isset($oldSet)) {
|
|
||||||
$oldSet->setName($newSet->getName());
|
|
||||||
$oldSet->setDescription($newSet->getDescription());
|
|
||||||
} else {
|
|
||||||
$this->entityManager->persist($newSet);
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete metadata format and all associated records.
|
|
||||||
*
|
|
||||||
* @param Format $format The metadata format
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function deleteMetadataFormat(Format $format): void
|
|
||||||
{
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->delete(Record::class, 'record')
|
|
||||||
->where($dql->expr()->eq('record.format', ':format'))
|
|
||||||
->setParameter('format', $format->getPrefix());
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
// Explicitly remove associations with sets for deleted records.
|
|
||||||
$sql = $this->entityManager->getConnection();
|
|
||||||
$sql->executeStatement("DELETE FROM records_sets WHERE record_format='{$format->getPrefix()}'");
|
|
||||||
|
|
||||||
$this->entityManager->remove($format);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
$this->pruneOrphanSets();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a record.
|
|
||||||
*
|
|
||||||
* @param Record $record The record
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function deleteRecord(Record $record): void
|
|
||||||
{
|
|
||||||
if (Configuration::getInstance()->deletedRecords === 'no') {
|
|
||||||
$this->entityManager->remove($record);
|
|
||||||
} else {
|
|
||||||
$record->setContent(null);
|
|
||||||
$record->setLastChanged(new DateTime());
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$this->pruneOrphanSets();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flush all changes to the database.
|
|
||||||
*
|
|
||||||
* @param bool $clear Should the entity manager get cleared as well?
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function flush(bool $clear = false): void
|
|
||||||
{
|
|
||||||
$this->entityManager->flush();
|
|
||||||
if ($clear) {
|
|
||||||
$this->entityManager->clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all sets without pagination.
|
|
||||||
*
|
|
||||||
* @return Result<Sets> The sets
|
|
||||||
*/
|
|
||||||
public function getAllSets(): Result
|
|
||||||
{
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select('sets')
|
|
||||||
->from(Set::class, 'sets', 'sets.spec');
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
$query->enableResultCache();
|
|
||||||
/** @var Sets $resultQuery */
|
|
||||||
$resultQuery = $query->getResult();
|
|
||||||
return new Result($resultQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the earliest datestamp of any record.
|
|
||||||
*
|
|
||||||
* @return string The earliest datestamp
|
|
||||||
*/
|
|
||||||
public function getEarliestDatestamp(): string
|
|
||||||
{
|
|
||||||
$timestamp = '0000-00-00T00:00:00Z';
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select($dql->expr()->min('record.lastChanged'))
|
|
||||||
->from(Record::class, 'record');
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
$query->enableResultCache();
|
|
||||||
/** @var ?string $result */
|
|
||||||
$result = $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
|
|
||||||
return $result ?? $timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Doctrine entity manager.
|
|
||||||
*
|
|
||||||
* @return EntityManager The entity manager instance
|
|
||||||
*/
|
|
||||||
public function getEntityManager(): EntityManager
|
|
||||||
{
|
|
||||||
return $this->entityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all metadata prefixes.
|
|
||||||
*
|
|
||||||
* @param ?string $identifier Optional record identifier
|
|
||||||
*
|
|
||||||
* @return Result<Formats> The metadata prefixes
|
|
||||||
*/
|
|
||||||
public function getMetadataFormats(?string $identifier = null): Result
|
|
||||||
{
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select('format')
|
|
||||||
->from(Format::class, 'format', 'format.prefix');
|
|
||||||
if (isset($identifier)) {
|
|
||||||
$dql->innerJoin(Record::class, 'record')
|
|
||||||
->where(
|
|
||||||
$dql->expr()->andX(
|
|
||||||
$dql->expr()->eq('record.identifier', ':identifier'),
|
|
||||||
$dql->expr()->isNotNull('record.content')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
->setParameter('identifier', $identifier);
|
|
||||||
}
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
$query->enableResultCache();
|
|
||||||
/** @var Formats $queryResult */
|
|
||||||
$queryResult = $query->getResult();
|
|
||||||
return new Result($queryResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single record.
|
|
||||||
*
|
|
||||||
* @param string $identifier The record identifier
|
|
||||||
* @param Format $format The metadata format
|
|
||||||
*
|
|
||||||
* @return ?Record The record or NULL on failure
|
|
||||||
*/
|
|
||||||
public function getRecord(string $identifier, Format $format): ?Record
|
|
||||||
{
|
|
||||||
return $this->entityManager->find(
|
|
||||||
Record::class,
|
|
||||||
[
|
|
||||||
'identifier' => $identifier,
|
|
||||||
'format' => $format
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of records.
|
|
||||||
*
|
|
||||||
* @param string $verb The currently requested verb ('ListIdentifiers' or 'ListRecords')
|
|
||||||
* @param Format $metadataPrefix The metadata format
|
|
||||||
* @param int $counter Counter for split result sets
|
|
||||||
* @param ?DateTime $from The "from" datestamp
|
|
||||||
* @param ?DateTime $until The "until" datestamp
|
|
||||||
* @param ?Set $set The set spec
|
|
||||||
*
|
|
||||||
* @return Result<Records> The records and possibly a resumtion token
|
|
||||||
*/
|
|
||||||
public function getRecords(
|
|
||||||
string $verb,
|
|
||||||
Format $metadataPrefix,
|
|
||||||
int $counter = 0,
|
|
||||||
?DateTime $from = null,
|
|
||||||
?DateTime $until = null,
|
|
||||||
?Set $set = null
|
|
||||||
): Result {
|
|
||||||
$maxRecords = Configuration::getInstance()->maxRecords;
|
|
||||||
$cursor = $counter * $maxRecords;
|
|
||||||
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select('record')
|
|
||||||
->from(Record::class, 'record', 'record.identifier')
|
|
||||||
->where($dql->expr()->eq('record.format', ':metadataPrefix'))
|
|
||||||
->setParameter('metadataPrefix', $metadataPrefix)
|
|
||||||
->setFirstResult($cursor)
|
|
||||||
->setMaxResults($maxRecords);
|
|
||||||
if (isset($from)) {
|
|
||||||
$dql->andWhere($dql->expr()->gte('record.lastChanged', ':from'));
|
|
||||||
$dql->setParameter('from', $from);
|
|
||||||
$from = $from->format('Y-m-d\TH:i:s\Z');
|
|
||||||
}
|
|
||||||
if (isset($until)) {
|
|
||||||
$dql->andWhere($dql->expr()->lte('record.lastChanged', ':until'));
|
|
||||||
$dql->setParameter('until', $until);
|
|
||||||
$until = $until->format('Y-m-d\TH:i:s\Z');
|
|
||||||
}
|
|
||||||
if (isset($set)) {
|
|
||||||
$dql->innerJoin(
|
|
||||||
Set::class,
|
|
||||||
'sets',
|
|
||||||
Join::WITH,
|
|
||||||
$dql->expr()->orX(
|
|
||||||
$dql->expr()->eq('sets.spec', ':setSpec'),
|
|
||||||
$dql->expr()->like('sets.spec', ':setLike')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$dql->setParameter('setSpec', $set->getSpec());
|
|
||||||
$dql->setParameter('setLike', $set->getSpec() . ':%');
|
|
||||||
$set = $set->getSpec();
|
|
||||||
}
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
/** @var Records $queryResult */
|
|
||||||
$queryResult = $query->getResult();
|
|
||||||
$result = new Result($queryResult);
|
|
||||||
$paginator = new Paginator($query, true);
|
|
||||||
if (count($paginator) > ($cursor + count($result))) {
|
|
||||||
$token = new Token($verb, [
|
|
||||||
'counter' => $counter + 1,
|
|
||||||
'completeListSize' => count($paginator),
|
|
||||||
'metadataPrefix' => $metadataPrefix->getPrefix(),
|
|
||||||
'from' => $from,
|
|
||||||
'until' => $until,
|
|
||||||
'set' => $set
|
|
||||||
]);
|
|
||||||
$this->entityManager->persist($token);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$result->setResumptionToken($token);
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get resumption token.
|
|
||||||
*
|
|
||||||
* @param string $token The token
|
|
||||||
* @param string $verb The current verb to validate token
|
|
||||||
*
|
|
||||||
* @return ?Token The resumption token or NULL if invalid
|
|
||||||
*/
|
|
||||||
public function getResumptionToken(string $token, string $verb): ?Token
|
|
||||||
{
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select('token')
|
|
||||||
->from(Token::class, 'token')
|
|
||||||
->where($dql->expr()->gte('token.validUntil', ':now'))
|
|
||||||
->andWhere($dql->expr()->eq('token.token', ':token'))
|
|
||||||
->andWhere($dql->expr()->eq('token.verb', ':verb'))
|
|
||||||
->setParameter('now', new DateTime())
|
|
||||||
->setParameter('token', $token)
|
|
||||||
->setParameter('verb', $verb)
|
|
||||||
->setMaxResults(1);
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
/** @var ?Token */
|
|
||||||
return $query->getOneOrNullResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all sets.
|
|
||||||
*
|
|
||||||
* @param int $counter Counter for split result sets
|
|
||||||
*
|
|
||||||
* @return Result<Sets> The sets and possibly a resumption token
|
|
||||||
*/
|
|
||||||
public function getSets(int $counter = 0): Result
|
|
||||||
{
|
|
||||||
$maxRecords = Configuration::getInstance()->maxRecords;
|
|
||||||
$cursor = $counter * $maxRecords;
|
|
||||||
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select('sets')
|
|
||||||
->from(Set::class, 'sets', 'sets.spec')
|
|
||||||
->setFirstResult($cursor)
|
|
||||||
->setMaxResults($maxRecords);
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
$query->enableResultCache();
|
|
||||||
/** @var Sets $queryResult */
|
|
||||||
$queryResult = $query->getResult();
|
|
||||||
$result = new Result($queryResult);
|
|
||||||
$paginator = new Paginator($query, false);
|
|
||||||
if (count($paginator) > ($cursor + count($result))) {
|
|
||||||
$token = new Token('ListSets', [
|
|
||||||
'counter' => $counter + 1,
|
|
||||||
'completeListSize' => count($paginator)
|
|
||||||
]);
|
|
||||||
$this->entityManager->persist($token);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$result->setResumptionToken($token);
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a record identifier exists.
|
|
||||||
*
|
|
||||||
* @param string $identifier The record identifier
|
|
||||||
*
|
|
||||||
* @return bool Whether the identifier exists
|
|
||||||
*/
|
|
||||||
public function idDoesExist(string $identifier): bool
|
|
||||||
{
|
|
||||||
$dql = $this->entityManager->createQueryBuilder();
|
|
||||||
$dql->select($dql->expr()->count('record.identifier'))
|
|
||||||
->from(Record::class, 'record')
|
|
||||||
->where($dql->expr()->eq('record.identifier', ':identifier'))
|
|
||||||
->setParameter('identifier', $identifier);
|
|
||||||
$query = $dql->getQuery();
|
|
||||||
return (bool) $query->getOneOrNullResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prune deleted records.
|
|
||||||
*
|
|
||||||
* @return int The number of removed records
|
|
||||||
*/
|
|
||||||
public function pruneDeletedRecords(): int
|
|
||||||
{
|
|
||||||
$repository = $this->entityManager->getRepository(Record::class);
|
|
||||||
$criteria = Criteria::create()->where(Criteria::expr()->isNull('content'));
|
|
||||||
$records = $repository->matching($criteria);
|
|
||||||
foreach ($records as $record) {
|
|
||||||
$this->entityManager->remove($record);
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
$this->pruneOrphanSets();
|
|
||||||
return count($records);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prune orphan sets.
|
|
||||||
*
|
|
||||||
* @return int The number of removed sets
|
|
||||||
*/
|
|
||||||
public function pruneOrphanSets(): int
|
|
||||||
{
|
|
||||||
$repository = $this->entityManager->getRepository(Set::class);
|
|
||||||
$sets = $repository->findAll();
|
|
||||||
$count = 0;
|
|
||||||
foreach ($sets as $set) {
|
|
||||||
if ($set->isEmpty()) {
|
|
||||||
$this->entityManager->remove($set);
|
|
||||||
++$count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
return $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prune expired resumption tokens.
|
|
||||||
*
|
|
||||||
* @return int The number of deleted tokens
|
|
||||||
*/
|
|
||||||
public function pruneResumptionTokens(): int
|
|
||||||
{
|
|
||||||
$repository = $this->entityManager->getRepository(Token::class);
|
|
||||||
$criteria = Criteria::create()->where(Criteria::expr()->lt('validUntil', new DateTime()));
|
|
||||||
$tokens = $repository->matching($criteria);
|
|
||||||
foreach ($tokens as $token) {
|
|
||||||
$this->entityManager->remove($token);
|
|
||||||
}
|
|
||||||
$this->entityManager->flush();
|
|
||||||
return count($tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a singleton class, thus the constructor is private.
|
|
||||||
*
|
|
||||||
* Usage: Get an instance of this class by calling Database::getInstance()
|
|
||||||
*/
|
|
||||||
private function __construct()
|
|
||||||
{
|
|
||||||
$configuration = new DoctrineConfiguration();
|
|
||||||
$configuration->setAutoGenerateProxyClasses(
|
|
||||||
ProxyFactory::AUTOGENERATE_NEVER
|
|
||||||
);
|
|
||||||
$configuration->setMetadataCache(
|
|
||||||
new PhpFilesAdapter(
|
|
||||||
'Metadata',
|
|
||||||
0,
|
|
||||||
__DIR__ . '/../var/cache'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$configuration->setMetadataDriverImpl(
|
|
||||||
new AttributeDriver([__DIR__ . '/Entity'])
|
|
||||||
);
|
|
||||||
$configuration->setProxyDir(__DIR__ . '/../var/generated');
|
|
||||||
$configuration->setProxyNamespace('OCC\OaiPmh2\Entity\Proxy');
|
|
||||||
$configuration->setQueryCache(
|
|
||||||
new PhpFilesAdapter(
|
|
||||||
'Query',
|
|
||||||
0,
|
|
||||||
__DIR__ . '/../var/cache'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$configuration->setResultCache(
|
|
||||||
new PhpFilesAdapter(
|
|
||||||
'Result',
|
|
||||||
0,
|
|
||||||
__DIR__ . '/../var/cache'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$configuration->setSchemaAssetsFilter(
|
|
||||||
static function (string|AbstractAsset $assetName): bool {
|
|
||||||
if ($assetName instanceof AbstractAsset) {
|
|
||||||
$assetName = $assetName->getName();
|
|
||||||
}
|
|
||||||
return in_array($assetName, self::DB_TABLES, true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$baseDir = Path::canonicalize(__DIR__ . '/../');
|
|
||||||
$dsn = str_replace('%BASEDIR%', $baseDir, Configuration::getInstance()->database);
|
|
||||||
$parser = new DsnParser([
|
|
||||||
'mariadb' => 'pdo_mysql',
|
|
||||||
'mssql' => 'pdo_sqlsrv',
|
|
||||||
'mysql' => 'pdo_mysql',
|
|
||||||
'oracle' => 'pdo_oci',
|
|
||||||
'postgresql' => 'pdo_pgsql',
|
|
||||||
'sqlite' => 'pdo_sqlite'
|
|
||||||
]);
|
|
||||||
$connection = DriverManager::getConnection($parser->parse($dsn), $configuration);
|
|
||||||
|
|
||||||
$this->entityManager = new EntityManager($connection, $configuration);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,9 +22,10 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2;
|
namespace OCC\OaiPmh2;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use OCC\OaiPmh2\Validator\RegExValidator;
|
||||||
|
use OCC\OaiPmh2\Validator\UrlValidator;
|
||||||
|
use OCC\OaiPmh2\Validator\XmlValidator;
|
||||||
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
use Symfony\Component\Validator\Validation;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all Doctrine/ORM entities.
|
* Base class for all Doctrine/ORM entities.
|
||||||
|
@ -45,17 +46,13 @@ abstract class Entity
|
||||||
*/
|
*/
|
||||||
protected function validateUrl(string $url): string
|
protected function validateUrl(string $url): string
|
||||||
{
|
{
|
||||||
$url = trim($url);
|
$url = trim(string: $url);
|
||||||
$validator = Validation::createValidator();
|
$violations = UrlValidator::validate(url: $url);
|
||||||
$violations = $validator->validate(
|
|
||||||
$url,
|
|
||||||
[
|
|
||||||
new Assert\Url(),
|
|
||||||
new Assert\NotBlank()
|
|
||||||
]
|
|
||||||
);
|
|
||||||
if ($violations->count() > 0) {
|
if ($violations->count() > 0) {
|
||||||
throw new ValidationFailedException(null, $violations);
|
throw new ValidationFailedException(
|
||||||
|
value: null,
|
||||||
|
violations: $violations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
|
@ -72,18 +69,12 @@ abstract class Entity
|
||||||
*/
|
*/
|
||||||
protected function validateRegEx(string $string, string $regEx): string
|
protected function validateRegEx(string $string, string $regEx): string
|
||||||
{
|
{
|
||||||
$validator = Validation::createValidator();
|
$violations = RegExValidator::validate(string: $string, regEx: $regEx);
|
||||||
$violations = $validator->validate(
|
|
||||||
$string,
|
|
||||||
[
|
|
||||||
new Assert\Regex([
|
|
||||||
'pattern' => $regEx,
|
|
||||||
'message' => 'This value does not match the regular expression "{{ pattern }}".'
|
|
||||||
])
|
|
||||||
]
|
|
||||||
);
|
|
||||||
if ($violations->count() > 0) {
|
if ($violations->count() > 0) {
|
||||||
throw new ValidationFailedException(null, $violations);
|
throw new ValidationFailedException(
|
||||||
|
value: null,
|
||||||
|
violations: $violations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
@ -99,19 +90,12 @@ abstract class Entity
|
||||||
*/
|
*/
|
||||||
protected function validateXml(string $xml): string
|
protected function validateXml(string $xml): string
|
||||||
{
|
{
|
||||||
$validator = Validation::createValidator();
|
$violations = XmlValidator::validate(xml: $xml);
|
||||||
$violations = $validator->validate(
|
if ($violations->count() > 0) {
|
||||||
$xml,
|
throw new ValidationFailedException(
|
||||||
[
|
value: null,
|
||||||
new Assert\Type('string'),
|
violations: $violations
|
||||||
new Assert\NotBlank()
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
if (
|
|
||||||
$violations->count() > 0
|
|
||||||
or simplexml_load_string($xml) === false
|
|
||||||
) {
|
|
||||||
throw new ValidationFailedException(null, $violations);
|
|
||||||
}
|
}
|
||||||
return $xml;
|
return $xml;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,11 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Entity;
|
namespace OCC\OaiPmh2\Entity;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use OCC\OaiPmh2\Entity;
|
use OCC\OaiPmh2\Entity;
|
||||||
|
use OCC\OaiPmh2\Repository\FormatRepository;
|
||||||
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,9 +35,9 @@ use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: FormatRepository::class)]
|
||||||
#[ORM\Table(name: 'formats')]
|
#[ORM\Table(name: 'formats')]
|
||||||
class Format extends Entity
|
final class Format extends Entity
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The unique metadata prefix.
|
* The unique metadata prefix.
|
||||||
|
@ -56,7 +59,21 @@ class Format extends Entity
|
||||||
private string $xmlSchema;
|
private string $xmlSchema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the format's namespace URI.
|
* The format's associated records.
|
||||||
|
*
|
||||||
|
* @var Collection<string, Record>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(
|
||||||
|
targetEntity: Record::class,
|
||||||
|
mappedBy: 'format',
|
||||||
|
fetch: 'EXTRA_LAZY',
|
||||||
|
orphanRemoval: true,
|
||||||
|
indexBy: 'identifier'
|
||||||
|
)]
|
||||||
|
private Collection $records;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the namespace URI for this format.
|
||||||
*
|
*
|
||||||
* @return string The namespace URI
|
* @return string The namespace URI
|
||||||
*/
|
*/
|
||||||
|
@ -76,7 +93,17 @@ class Format extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the format's schema URL.
|
* Get the associated records for this format.
|
||||||
|
*
|
||||||
|
* @return Collection<string, Record> The collection of records
|
||||||
|
*/
|
||||||
|
public function getRecords(): Collection
|
||||||
|
{
|
||||||
|
return $this->records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the schema URL for this format.
|
||||||
*
|
*
|
||||||
* @return string The schema URL
|
* @return string The schema URL
|
||||||
*/
|
*/
|
||||||
|
@ -86,7 +113,7 @@ class Format extends Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the format's namespace URI.
|
* Set the namespace URI for this format.
|
||||||
*
|
*
|
||||||
* @param string $namespace The namespace URI
|
* @param string $namespace The namespace URI
|
||||||
*
|
*
|
||||||
|
@ -97,14 +124,14 @@ class Format extends Entity
|
||||||
public function setNamespace(string $namespace): void
|
public function setNamespace(string $namespace): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->namespace = $this->validateUrl($namespace);
|
$this->namespace = $this->validateUrl(url: $namespace);
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the format's schema URL.
|
* Set the schema URL for this format.
|
||||||
*
|
*
|
||||||
* @param string $schema The schema URL
|
* @param string $schema The schema URL
|
||||||
*
|
*
|
||||||
|
@ -115,7 +142,7 @@ class Format extends Entity
|
||||||
public function setSchema(string $schema): void
|
public function setSchema(string $schema): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->xmlSchema = $this->validateUrl($schema);
|
$this->xmlSchema = $this->validateUrl(url: $schema);
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
@ -133,9 +160,13 @@ class Format extends Entity
|
||||||
public function __construct(string $prefix, string $namespace, string $schema)
|
public function __construct(string $prefix, string $namespace, string $schema)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->prefix = $this->validateRegEx($prefix, '/^[A-Za-z0-9\-_\.!~\*\'\(\)]+$/');
|
$this->prefix = $this->validateRegEx(
|
||||||
$this->setNamespace($namespace);
|
string: $prefix,
|
||||||
$this->setSchema($schema);
|
regEx: '/^[A-Za-z0-9\-_\.!~\*\'\(\)]+$/'
|
||||||
|
);
|
||||||
|
$this->setNamespace(namespace: $namespace);
|
||||||
|
$this->setSchema(schema: $schema);
|
||||||
|
$this->records = new ArrayCollection();
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use OCC\OaiPmh2\Entity;
|
use OCC\OaiPmh2\Entity;
|
||||||
|
use OCC\OaiPmh2\Repository\RecordRepository;
|
||||||
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,13 +36,13 @@ use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: RecordRepository::class)]
|
||||||
#[ORM\Table(name: 'records')]
|
#[ORM\Table(name: 'records')]
|
||||||
#[ORM\Index(name: 'identifier_idx', columns: ['identifier'])]
|
#[ORM\Index(name: 'identifier_idx', columns: ['identifier'])]
|
||||||
#[ORM\Index(name: 'format_idx', columns: ['format'])]
|
#[ORM\Index(name: 'format_idx', columns: ['format'])]
|
||||||
#[ORM\Index(name: 'last_changed_idx', columns: ['last_changed'])]
|
#[ORM\Index(name: 'last_changed_idx', columns: ['last_changed'])]
|
||||||
#[ORM\Index(name: 'format_last_changed_idx', columns: ['format', 'last_changed'])]
|
#[ORM\Index(name: 'format_last_changed_idx', columns: ['format', 'last_changed'])]
|
||||||
class Record extends Entity
|
final class Record extends Entity
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The record identifier.
|
* The record identifier.
|
||||||
|
@ -54,8 +55,16 @@ class Record extends Entity
|
||||||
* The associated format.
|
* The associated format.
|
||||||
*/
|
*/
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\ManyToOne(targetEntity: Format::class, fetch: 'EXTRA_LAZY')]
|
#[ORM\ManyToOne(
|
||||||
#[ORM\JoinColumn(name: 'format', referencedColumnName: 'prefix')]
|
targetEntity: Format::class,
|
||||||
|
fetch: 'EXTRA_LAZY',
|
||||||
|
inversedBy: 'records'
|
||||||
|
)]
|
||||||
|
#[ORM\JoinColumn(
|
||||||
|
name: 'format',
|
||||||
|
referencedColumnName: 'prefix',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
)]
|
||||||
private Format $format;
|
private Format $format;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +84,13 @@ class Record extends Entity
|
||||||
*
|
*
|
||||||
* @var Collection<string, Set>
|
* @var Collection<string, Set>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Set::class, inversedBy: 'records', indexBy: 'spec', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
#[ORM\ManyToMany(
|
||||||
|
targetEntity: Set::class,
|
||||||
|
inversedBy: 'records',
|
||||||
|
cascade: ['persist'],
|
||||||
|
fetch: 'EXTRA_LAZY',
|
||||||
|
indexBy: 'spec'
|
||||||
|
)]
|
||||||
#[ORM\JoinTable(name: 'records_sets')]
|
#[ORM\JoinTable(name: 'records_sets')]
|
||||||
#[ORM\JoinColumn(name: 'record_identifier', referencedColumnName: 'identifier')]
|
#[ORM\JoinColumn(name: 'record_identifier', referencedColumnName: 'identifier')]
|
||||||
#[ORM\JoinColumn(name: 'record_format', referencedColumnName: 'format')]
|
#[ORM\JoinColumn(name: 'record_format', referencedColumnName: 'format')]
|
||||||
|
@ -91,9 +106,9 @@ class Record extends Entity
|
||||||
*/
|
*/
|
||||||
public function addSet(Set $set): void
|
public function addSet(Set $set): void
|
||||||
{
|
{
|
||||||
if (!$this->sets->contains($set)) {
|
if (!$this->sets->contains(element: $set)) {
|
||||||
$this->sets->add($set);
|
$this->sets->add(element: $set);
|
||||||
$set->addRecord($this);
|
$set->addRecord(record: $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,23 +161,26 @@ class Record extends Entity
|
||||||
*/
|
*/
|
||||||
public function getSet(string $setSpec): ?Set
|
public function getSet(string $setSpec): ?Set
|
||||||
{
|
{
|
||||||
return $this->sets->get($setSpec);
|
return $this->sets->get(key: $setSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a collection of associated sets.
|
* Get a collection of associated sets.
|
||||||
*
|
*
|
||||||
* @return array<string, Set> The associated sets
|
* @return Collection<string, Set> The associated sets
|
||||||
*/
|
*/
|
||||||
public function getSets(): array
|
public function getSets(): Collection
|
||||||
{
|
{
|
||||||
return $this->sets->toArray();
|
return $this->sets;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this record has any content.
|
* Whether this record has any content.
|
||||||
*
|
*
|
||||||
* @return bool TRUE if content exists, FALSE otherwise
|
* @return bool TRUE if content exists, FALSE otherwise
|
||||||
|
*
|
||||||
|
* @psalm-assert-if-true string $this->content
|
||||||
|
* @psalm-assert-if-true string $this->getContent()
|
||||||
*/
|
*/
|
||||||
public function hasContent(): bool
|
public function hasContent(): bool
|
||||||
{
|
{
|
||||||
|
@ -178,9 +196,9 @@ class Record extends Entity
|
||||||
*/
|
*/
|
||||||
public function removeSet(Set $set): void
|
public function removeSet(Set $set): void
|
||||||
{
|
{
|
||||||
if ($this->sets->contains($set)) {
|
if ($this->sets->contains(element: $set)) {
|
||||||
$this->sets->removeElement($set);
|
$this->sets->removeElement(element: $set);
|
||||||
$set->removeRecord($this);
|
$set->removeRecord(record: $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +218,7 @@ class Record extends Entity
|
||||||
$data = trim($data);
|
$data = trim($data);
|
||||||
if ($validate) {
|
if ($validate) {
|
||||||
try {
|
try {
|
||||||
$data = $this->validateXml($data);
|
$data = $this->validateXml(xml: $data);
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
@ -250,13 +268,13 @@ class Record extends Entity
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->identifier = $this->validateRegEx(
|
$this->identifier = $this->validateRegEx(
|
||||||
$identifier,
|
string: $identifier,
|
||||||
// xs:anyURI
|
// xs:anyURI
|
||||||
'/^(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?\/{0,2}[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?(#[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?$/'
|
regEx: '/^(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)?\/{0,2}[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?(#[0-9a-zA-Z;\/?:@&=+$\\.\\-_!~*\'()%]+)?$/'
|
||||||
);
|
);
|
||||||
$this->setFormat($format);
|
$this->setFormat(format: $format);
|
||||||
$this->setContent($data);
|
$this->setContent(data: $data);
|
||||||
$this->setLastChanged($lastChanged);
|
$this->setLastChanged(dateTime: $lastChanged);
|
||||||
$this->sets = new ArrayCollection();
|
$this->sets = new ArrayCollection();
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
|
|
|
@ -26,6 +26,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use OCC\OaiPmh2\Entity;
|
use OCC\OaiPmh2\Entity;
|
||||||
|
use OCC\OaiPmh2\Repository\SetRepository;
|
||||||
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,9 +35,9 @@ use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: SetRepository::class)]
|
||||||
#[ORM\Table(name: 'sets')]
|
#[ORM\Table(name: 'sets')]
|
||||||
class Set extends Entity
|
final class Set extends Entity
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The unique set spec.
|
* The unique set spec.
|
||||||
|
@ -60,9 +61,14 @@ class Set extends Entity
|
||||||
/**
|
/**
|
||||||
* Collection of associated records.
|
* Collection of associated records.
|
||||||
*
|
*
|
||||||
* @var Collection<int, Record>
|
* @var Collection<string, Record>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: Record::class, mappedBy: 'sets', fetch: 'EXTRA_LAZY')]
|
#[ORM\ManyToMany(
|
||||||
|
targetEntity: Record::class,
|
||||||
|
mappedBy: 'sets',
|
||||||
|
fetch: 'EXTRA_LAZY',
|
||||||
|
indexBy: 'identifier'
|
||||||
|
)]
|
||||||
private Collection $records;
|
private Collection $records;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,9 +80,9 @@ class Set extends Entity
|
||||||
*/
|
*/
|
||||||
public function addRecord(Record $record): void
|
public function addRecord(Record $record): void
|
||||||
{
|
{
|
||||||
if (!$this->records->contains($record)) {
|
if (!$this->records->contains(element: $record)) {
|
||||||
$this->records->add($record);
|
$this->records->add(element: $record);
|
||||||
$record->addSet($this);
|
$record->addSet(set: $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,17 +119,20 @@ class Set extends Entity
|
||||||
/**
|
/**
|
||||||
* Get a collection of associated records.
|
* Get a collection of associated records.
|
||||||
*
|
*
|
||||||
* @return array<int, Record> The associated records
|
* @return Collection<string, Record> The associated records
|
||||||
*/
|
*/
|
||||||
public function getRecords(): array
|
public function getRecords(): Collection
|
||||||
{
|
{
|
||||||
return $this->records->toArray();
|
return $this->records;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this set has a description.
|
* Whether this set has a description.
|
||||||
*
|
*
|
||||||
* @return bool TRUE if description exists, FALSE otherwise
|
* @return bool TRUE if description exists, FALSE otherwise
|
||||||
|
*
|
||||||
|
* @psalm-assert-if-true string $this->description
|
||||||
|
* @psalm-assert-if-true string $this->getDescription()
|
||||||
*/
|
*/
|
||||||
public function hasDescription(): bool
|
public function hasDescription(): bool
|
||||||
{
|
{
|
||||||
|
@ -149,16 +158,16 @@ class Set extends Entity
|
||||||
*/
|
*/
|
||||||
public function removeRecord(Record $record): void
|
public function removeRecord(Record $record): void
|
||||||
{
|
{
|
||||||
if ($this->records->contains($record)) {
|
if ($this->records->contains(element: $record)) {
|
||||||
$this->records->removeElement($record);
|
$this->records->removeElement(element: $record);
|
||||||
$record->removeSet($this);
|
$record->removeSet(set: $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the description for this set.
|
* Set the description for this set.
|
||||||
*
|
*
|
||||||
* @param ?string $description The description
|
* @param ?string $description The description XML or NULL to unset
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
|
@ -169,7 +178,7 @@ class Set extends Entity
|
||||||
if (isset($description)) {
|
if (isset($description)) {
|
||||||
$description = trim($description);
|
$description = trim($description);
|
||||||
try {
|
try {
|
||||||
$description = $this->validateXml($description);
|
$description = $this->validateXml(xml: $description);
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
|
@ -198,15 +207,15 @@ class Set extends Entity
|
||||||
*
|
*
|
||||||
* @throws ValidationFailedException
|
* @throws ValidationFailedException
|
||||||
*/
|
*/
|
||||||
public function __construct(string $spec, ?string $name = null, string $description = null)
|
public function __construct(string $spec, ?string $name = null, ?string $description = null)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->spec = $this->validateRegEx(
|
$this->spec = $this->validateRegEx(
|
||||||
$spec,
|
string: $spec,
|
||||||
'/^([A-Za-z0-9\-_\.!~\*\'\(\)])+(:[A-Za-z0-9\-_\.!~\*\'\(\)]+)*$/'
|
regEx: '/^([A-Za-z0-9\-_\.!~\*\'\(\)])+(:[A-Za-z0-9\-_\.!~\*\'\(\)]+)*$/'
|
||||||
);
|
);
|
||||||
$this->setName($name);
|
$this->setName(name: $name);
|
||||||
$this->setDescription($description);
|
$this->setDescription(description: $description);
|
||||||
$this->records = new ArrayCollection();
|
$this->records = new ArrayCollection();
|
||||||
} catch (ValidationFailedException $exception) {
|
} catch (ValidationFailedException $exception) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
|
|
|
@ -27,17 +27,20 @@ use DateTime;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use OCC\OaiPmh2\Configuration;
|
use OCC\OaiPmh2\Configuration;
|
||||||
use OCC\OaiPmh2\Entity;
|
use OCC\OaiPmh2\Entity;
|
||||||
|
use OCC\OaiPmh2\Repository\TokenRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Doctrine/ORM Entity for resumption tokens.
|
* Doctrine/ORM Entity for resumption tokens.
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @psalm-import-type OaiRequestMetadata from \OCC\OaiPmh2\Middleware
|
||||||
*/
|
*/
|
||||||
#[ORM\Entity]
|
#[ORM\Entity(repositoryClass: TokenRepository::class)]
|
||||||
#[ORM\Table(name: 'tokens')]
|
#[ORM\Table(name: 'tokens')]
|
||||||
#[ORM\Index(name: 'valid_until_idx', columns: ['valid_until'])]
|
#[ORM\Index(name: 'valid_until_idx', columns: ['valid_until'])]
|
||||||
class Token extends Entity
|
final class Token extends Entity
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The resumption token.
|
* The resumption token.
|
||||||
|
@ -77,11 +80,11 @@ class Token extends Entity
|
||||||
/**
|
/**
|
||||||
* Get the query parameters.
|
* Get the query parameters.
|
||||||
*
|
*
|
||||||
* @return array<string, int|string|null> The query parameters
|
* @return OaiRequestMetadata The query parameters
|
||||||
*/
|
*/
|
||||||
public function getParameters(): array
|
public function getParameters(): array
|
||||||
{
|
{
|
||||||
/** @var array<string, int|string|null> */
|
/** @var OaiRequestMetadata */
|
||||||
return unserialize($this->parameters);
|
return unserialize($this->parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,15 +112,15 @@ class Token extends Entity
|
||||||
* Get new entity of resumption token.
|
* Get new entity of resumption token.
|
||||||
*
|
*
|
||||||
* @param string $verb The verb for which the token is issued
|
* @param string $verb The verb for which the token is issued
|
||||||
* @param array<string, int|string|null> $parameters The query parameters
|
* @param OaiRequestMetadata $parameters The query parameters
|
||||||
*/
|
*/
|
||||||
public function __construct(string $verb, array $parameters)
|
public function __construct(string $verb, array $parameters)
|
||||||
{
|
{
|
||||||
$this->token = substr(md5(microtime()), 0, 8);
|
$this->token = substr(md5(microtime()), 0, 8);
|
||||||
$this->verb = $verb;
|
$this->verb = $verb;
|
||||||
$this->parameters = serialize($parameters);
|
$this->parameters = serialize($parameters);
|
||||||
$validity = new DateTime();
|
$validUntil = new DateTime();
|
||||||
$validity->add(new DateInterval('PT' . Configuration::getInstance()->tokenValid . 'S'));
|
$validUntil->add(interval: new DateInterval(duration: 'PT' . Configuration::getInstance()->tokenValid . 'S'));
|
||||||
$this->validUntil = $validity;
|
$this->validUntil = $validUntil;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,471 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\DBAL\DriverManager;
|
||||||
|
use Doctrine\DBAL\Schema\AbstractAsset;
|
||||||
|
use Doctrine\DBAL\Tools\DsnParser;
|
||||||
|
use Doctrine\ORM\AbstractQuery;
|
||||||
|
use Doctrine\ORM\Configuration as DoctrineConfiguration;
|
||||||
|
use Doctrine\ORM\Decorator\EntityManagerDecorator;
|
||||||
|
use Doctrine\ORM\EntityManager as DoctrineEntityManager;
|
||||||
|
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||||
|
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||||
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
|
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||||
|
use OCC\Basics\Traits\Singleton;
|
||||||
|
use OCC\OaiPmh2\Entity\Format;
|
||||||
|
use OCC\OaiPmh2\Entity\Record;
|
||||||
|
use OCC\OaiPmh2\Entity\Set;
|
||||||
|
use OCC\OaiPmh2\Entity\Token;
|
||||||
|
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||||
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Entity Manager controls all database shenanigans.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @mixin DoctrineEntityManager
|
||||||
|
*
|
||||||
|
* @psalm-import-type Params from DriverManager
|
||||||
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
|
||||||
|
*/
|
||||||
|
final class EntityManager extends EntityManagerDecorator
|
||||||
|
{
|
||||||
|
use Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database tables this class is allowed to handle.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private const TABLES = [
|
||||||
|
'formats',
|
||||||
|
'records',
|
||||||
|
'records_sets',
|
||||||
|
'sets',
|
||||||
|
'tokens'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update entity.
|
||||||
|
*
|
||||||
|
* @param Format|Record|Set|Token $entity The entity
|
||||||
|
* @param bool $bulkMode Should we operate in bulk mode (no flush)?
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addOrUpdate(Format|Record|Set|Token $entity, bool $bulkMode = false): void
|
||||||
|
{
|
||||||
|
$this->getRepository(className: get_class($entity))->addOrUpdate(entity: $entity);
|
||||||
|
if (!$bulkMode) {
|
||||||
|
$this->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entity.
|
||||||
|
*
|
||||||
|
* @param Format|Record|Set|Token $entity The entity
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function delete(Format|Record|Set|Token $entity): void
|
||||||
|
{
|
||||||
|
$this->getRepository(className: get_class($entity))->delete(entity: $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the earliest datestamp of any record.
|
||||||
|
*
|
||||||
|
* @return string The earliest datestamp
|
||||||
|
*/
|
||||||
|
public function getEarliestDatestamp(): string
|
||||||
|
{
|
||||||
|
$timestamp = '0000-00-00T00:00:00Z';
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->select(select: $dql->expr()->min('record.lastChanged'));
|
||||||
|
$dql->from(from: Record::class, alias: 'record');
|
||||||
|
$query = $dql->getQuery()->enableResultCache();
|
||||||
|
/** @var ?string $result */
|
||||||
|
$result = $query->getOneOrNullResult(hydrationMode: AbstractQuery::HYDRATE_SCALAR_COLUMN);
|
||||||
|
return $result ?? $timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reference to a single metadata format.
|
||||||
|
*
|
||||||
|
* @param string $prefix The metadata prefix
|
||||||
|
*
|
||||||
|
* @return ?Format The reference to the metadata format or NULL if invalid
|
||||||
|
*/
|
||||||
|
public function getMetadataFormat(string $prefix): ?Format
|
||||||
|
{
|
||||||
|
return $this->getReference(entityName: Format::class, id: $prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available metadata formats (optionally for a given record identifier).
|
||||||
|
*
|
||||||
|
* @param ?string $recordIdentifier Optional record identifier
|
||||||
|
*
|
||||||
|
* @return ResultSet<Format> The metadata formats indexed by prefix
|
||||||
|
*/
|
||||||
|
public function getMetadataFormats(?string $recordIdentifier = null): ResultSet
|
||||||
|
{
|
||||||
|
$entities = [];
|
||||||
|
if ($recordIdentifier === null) {
|
||||||
|
$formats = $this->getRepository(className: Format::class)->findAll();
|
||||||
|
} else {
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->select(select: 'record.format')
|
||||||
|
->from(from: Record::class, alias: 'record')
|
||||||
|
->where(predicates: $dql->expr()->eq('record.identifier', ':recordIdentifier'))
|
||||||
|
->setParameter(key: 'recordIdentifier', value: $recordIdentifier);
|
||||||
|
$query = $dql->getQuery()->enableResultCache();
|
||||||
|
/** @var Format[] */
|
||||||
|
$formats = $query->getResult(hydrationMode: AbstractQuery::HYDRATE_OBJECT);
|
||||||
|
}
|
||||||
|
foreach ($formats as $format) {
|
||||||
|
$entities[$format->getPrefix()] = $format;
|
||||||
|
}
|
||||||
|
return new ResultSet(elements: $entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single record.
|
||||||
|
*
|
||||||
|
* @param string $identifier The record identifier
|
||||||
|
* @param string $format The metadata prefix
|
||||||
|
*
|
||||||
|
* @return ?Record The record or NULL if invalid
|
||||||
|
*/
|
||||||
|
public function getRecord(string $identifier, string $format): ?Record
|
||||||
|
{
|
||||||
|
return $this->getRepository(className: Record::class)->findOneBy(
|
||||||
|
criteria: [
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'format' => $this->getMetadataFormat(prefix: $format)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of records.
|
||||||
|
*
|
||||||
|
* @param string $verb The currently requested verb
|
||||||
|
* 'ListIdentifiers' or 'ListRecords'
|
||||||
|
* @param string $metadataPrefix The metadata prefix
|
||||||
|
* @param int $counter Counter for split result sets
|
||||||
|
* @param ?string $from The "from" datestamp
|
||||||
|
* @param ?string $until The "until" datestamp
|
||||||
|
* @param ?string $set The set spec
|
||||||
|
*
|
||||||
|
* @return ResultSet<Record> The records indexed by id and maybe a resumption token
|
||||||
|
*/
|
||||||
|
public function getRecords(
|
||||||
|
string $verb,
|
||||||
|
string $metadataPrefix,
|
||||||
|
int $counter = 0,
|
||||||
|
?string $from = null,
|
||||||
|
?string $until = null,
|
||||||
|
?string $set = null
|
||||||
|
): ResultSet {
|
||||||
|
$maxRecords = Configuration::getInstance()->maxRecords;
|
||||||
|
$cursor = $counter * $maxRecords;
|
||||||
|
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->select(select: 'record')
|
||||||
|
->from(from: Record::class, alias: 'record', indexBy: 'record.identifier')
|
||||||
|
->where(predicates: $dql->expr()->eq('record.format', ':metadataPrefix'))
|
||||||
|
->setParameter(
|
||||||
|
key: 'metadataPrefix',
|
||||||
|
value: $this->getMetadataFormat(prefix: $metadataPrefix)
|
||||||
|
)
|
||||||
|
->setFirstResult(firstResult: $cursor)
|
||||||
|
->setMaxResults(maxResults: $maxRecords);
|
||||||
|
if (isset($from)) {
|
||||||
|
$dql->andWhere(where: $dql->expr()->gte('record.lastChanged', ':from'));
|
||||||
|
$dql->setParameter(key: 'from', value: new DateTime($from));
|
||||||
|
}
|
||||||
|
if (isset($until)) {
|
||||||
|
$dql->andWhere(where: $dql->expr()->lte('record.lastChanged', ':until'));
|
||||||
|
$dql->setParameter(key: 'until', value: new DateTime($until));
|
||||||
|
}
|
||||||
|
if (isset($set)) {
|
||||||
|
$dql->innerJoin(
|
||||||
|
join: Set::class,
|
||||||
|
alias: 'sets',
|
||||||
|
conditionType: Join::WITH,
|
||||||
|
condition: $dql->expr()->orX(
|
||||||
|
$dql->expr()->eq('sets.spec', ':setSpec'),
|
||||||
|
$dql->expr()->like('sets.spec', ':setLike')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$dql->setParameter(key: 'setSpec', value: $set);
|
||||||
|
$dql->setParameter(key: 'setLike', value: $set . ':%');
|
||||||
|
}
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
/** @var array<string, Record> */
|
||||||
|
$queryResult = $query->getResult();
|
||||||
|
$result = new ResultSet(elements: $queryResult);
|
||||||
|
$paginator = new Paginator(query: $query, fetchJoinCollection: true);
|
||||||
|
if (count($paginator) > ($cursor + count($result))) {
|
||||||
|
$token = new Token(
|
||||||
|
verb: $verb,
|
||||||
|
parameters: [
|
||||||
|
'verb' => $verb,
|
||||||
|
'identifier' => null,
|
||||||
|
'metadataPrefix' => $metadataPrefix,
|
||||||
|
'from' => $from,
|
||||||
|
'until' => $until,
|
||||||
|
'set' => $set,
|
||||||
|
'resumptionToken' => null,
|
||||||
|
'counter' => $counter + 1,
|
||||||
|
'completeListSize' => count($paginator)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->persist(object: $token);
|
||||||
|
$this->flush();
|
||||||
|
$result->setResumptionToken(token: $token);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get resumption token.
|
||||||
|
*
|
||||||
|
* @param string $token The token
|
||||||
|
* @param string $verb The current verb to validate token
|
||||||
|
*
|
||||||
|
* @return ?Token The resumption token or NULL if invalid
|
||||||
|
*/
|
||||||
|
public function getResumptionToken(string $token, string $verb): ?Token
|
||||||
|
{
|
||||||
|
$resumptionToken = $this->getRepository(className: Token::class)->findOneBy(
|
||||||
|
criteria: [
|
||||||
|
'token' => $token,
|
||||||
|
'verb' => $verb
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (isset($resumptionToken) && $resumptionToken->getValidUntil() < new DateTime()) {
|
||||||
|
$this->delete(entity: $resumptionToken);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $resumptionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reference to a single set.
|
||||||
|
*
|
||||||
|
* @param string $spec The set spec
|
||||||
|
*
|
||||||
|
* @return ?Set The reference to the set or NULL if invalid
|
||||||
|
*/
|
||||||
|
public function getSet(string $spec): ?Set
|
||||||
|
{
|
||||||
|
return $this->getReference(entityName: Set::class, id: $spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available sets.
|
||||||
|
*
|
||||||
|
* @param int $counter Counter for split result sets
|
||||||
|
*
|
||||||
|
* @return ResultSet<Set> The sets indexed by spec
|
||||||
|
*/
|
||||||
|
public function getSets(int $counter = 0): ResultSet
|
||||||
|
{
|
||||||
|
$maxRecords = Configuration::getInstance()->maxRecords;
|
||||||
|
$cursor = $counter * $maxRecords;
|
||||||
|
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->select(select: 'set')
|
||||||
|
->from(from: Set::class, alias: 'set', indexBy: 'set.spec')
|
||||||
|
->setFirstResult(firstResult: $cursor)
|
||||||
|
->setMaxResults(maxResults: $maxRecords);
|
||||||
|
$query = $dql->getQuery()->enableResultCache();
|
||||||
|
/** @var array<string, Set> */
|
||||||
|
$queryResult = $query->getResult(hydrationMode: AbstractQuery::HYDRATE_OBJECT);
|
||||||
|
$result = new ResultSet(elements: $queryResult);
|
||||||
|
$paginator = new Paginator(query: $query);
|
||||||
|
if (count($paginator) > ($cursor + count($result))) {
|
||||||
|
$token = new Token(
|
||||||
|
verb: 'ListSets',
|
||||||
|
parameters: [
|
||||||
|
'verb' => 'ListSets',
|
||||||
|
'identifier' => null,
|
||||||
|
'metadataPrefix' => null,
|
||||||
|
'from' => null,
|
||||||
|
'until' => null,
|
||||||
|
'set' => null,
|
||||||
|
'resumptionToken' => null,
|
||||||
|
'counter' => $counter + 1,
|
||||||
|
'completeListSize' => count($paginator)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->persist(object: $token);
|
||||||
|
$this->flush();
|
||||||
|
$result->setResumptionToken(token: $token);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a record with the given identifier exists.
|
||||||
|
*
|
||||||
|
* @param string $identifier The record identifier
|
||||||
|
*
|
||||||
|
* @return bool Whether a record with the identifier exists
|
||||||
|
*/
|
||||||
|
public function isValidRecordIdentifier(string $identifier): bool
|
||||||
|
{
|
||||||
|
$records = $this->getRepository(className: Record::class)->findBy(criteria: ['identifier' => $identifier]);
|
||||||
|
return (bool) count($records) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune deleted records.
|
||||||
|
*
|
||||||
|
* @return int The number of removed records
|
||||||
|
*/
|
||||||
|
public function pruneDeletedRecords(): int
|
||||||
|
{
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->delete(delete: Record::class, alias: 'record')
|
||||||
|
->where(predicates: $dql->expr()->isNull('record.content'));
|
||||||
|
/** @var int */
|
||||||
|
$deleted = $dql->getQuery()->execute();
|
||||||
|
if ($deleted > 0) {
|
||||||
|
$this->pruneOrphanedSets();
|
||||||
|
}
|
||||||
|
return $deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune expired resumption tokens.
|
||||||
|
*
|
||||||
|
* @return int The number of deleted tokens
|
||||||
|
*/
|
||||||
|
public function pruneExpiredTokens(): int
|
||||||
|
{
|
||||||
|
$dql = $this->createQueryBuilder();
|
||||||
|
$dql->delete(delete: Token::class, alias: 'token')
|
||||||
|
->where(predicates: $dql->expr()->lt('token.validUntil', new DateTime()));
|
||||||
|
/** @var int */
|
||||||
|
return $dql->getQuery()->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune orphan sets.
|
||||||
|
*
|
||||||
|
* @return int The number of removed sets
|
||||||
|
*/
|
||||||
|
public function pruneOrphanedSets(): int
|
||||||
|
{
|
||||||
|
$sets = $this->getRepository(className: Set::class)->findAll();
|
||||||
|
$count = 0;
|
||||||
|
foreach ($sets as $set) {
|
||||||
|
if ($set->isEmpty()) {
|
||||||
|
$count += 1;
|
||||||
|
$this->remove(object: $set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($count > 0) {
|
||||||
|
$this->flush();
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate new Doctrine entity manager and connect to database.
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$config = new DoctrineConfiguration();
|
||||||
|
$config->setAutoGenerateProxyClasses(
|
||||||
|
autoGenerate: ProxyFactory::AUTOGENERATE_NEVER
|
||||||
|
);
|
||||||
|
$config->setMetadataCache(
|
||||||
|
cache: new PhpFilesAdapter(
|
||||||
|
namespace: 'Metadata',
|
||||||
|
directory: __DIR__ . '/../var/cache'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$config->setMetadataDriverImpl(
|
||||||
|
driverImpl: new AttributeDriver(
|
||||||
|
paths: [__DIR__ . '/Entity']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$config->setProxyDir(dir: __DIR__ . '/../var/generated');
|
||||||
|
$config->setProxyNamespace(ns: 'OCC\OaiPmh2\Entity\Proxy');
|
||||||
|
$config->setQueryCache(
|
||||||
|
cache: new PhpFilesAdapter(
|
||||||
|
namespace: 'Query',
|
||||||
|
directory: __DIR__ . '/../var/cache'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$config->setResultCache(
|
||||||
|
cache: new PhpFilesAdapter(
|
||||||
|
namespace: 'Result',
|
||||||
|
directory: __DIR__ . '/../var/cache'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$config->setSchemaAssetsFilter(
|
||||||
|
schemaAssetsFilter: static function (string|AbstractAsset $assetName): bool {
|
||||||
|
if ($assetName instanceof AbstractAsset) {
|
||||||
|
$assetName = $assetName->getName();
|
||||||
|
}
|
||||||
|
return in_array(needle: $assetName, haystack: self::TABLES, strict: true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$baseDir = Path::canonicalize(path: __DIR__ . '/../');
|
||||||
|
$dsn = str_replace(
|
||||||
|
search: '%BASEDIR%',
|
||||||
|
replace: $baseDir,
|
||||||
|
subject: Configuration::getInstance()->database
|
||||||
|
);
|
||||||
|
$parser = new DsnParser(
|
||||||
|
schemeMapping: [
|
||||||
|
'mariadb' => 'pdo_mysql',
|
||||||
|
'mssql' => 'pdo_sqlsrv',
|
||||||
|
'mysql' => 'pdo_mysql',
|
||||||
|
'oracle' => 'pdo_oci',
|
||||||
|
'postgresql' => 'pdo_pgsql',
|
||||||
|
'sqlite' => 'pdo_sqlite'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$conn = DriverManager::getConnection(
|
||||||
|
// Generic return type of DsnParser::parse() is not correctly recognized.
|
||||||
|
// phpcs:ignore
|
||||||
|
params: $parser->parse(dsn: $dsn),
|
||||||
|
config: $config
|
||||||
|
);
|
||||||
|
|
||||||
|
parent::__construct(new DoctrineEntityManager(conn: $conn, config: $config));
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2;
|
namespace OCC\OaiPmh2;
|
||||||
|
|
||||||
|
use DOMElement;
|
||||||
use GuzzleHttp\Psr7\Utils;
|
use GuzzleHttp\Psr7\Utils;
|
||||||
|
use OCC\OaiPmh2\Entity\Token;
|
||||||
use OCC\OaiPmh2\Middleware\ErrorHandler;
|
use OCC\OaiPmh2\Middleware\ErrorHandler;
|
||||||
use OCC\PSR15\AbstractMiddleware;
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
@ -33,13 +35,99 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @psalm-type OaiRequestMetadata = array{
|
||||||
|
* verb: string,
|
||||||
|
* identifier: ?string,
|
||||||
|
* metadataPrefix: ?string,
|
||||||
|
* from: ?string,
|
||||||
|
* until: ?string,
|
||||||
|
* set: ?string,
|
||||||
|
* resumptionToken: ?string,
|
||||||
|
* counter: int,
|
||||||
|
* completeListSize: int
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
abstract class Middleware extends AbstractMiddleware
|
abstract class Middleware extends AbstractMiddleware
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* This holds the request metadata.
|
||||||
|
*
|
||||||
|
* @var OaiRequestMetadata
|
||||||
|
*/
|
||||||
|
protected array $arguments = [
|
||||||
|
'verb' => '',
|
||||||
|
'identifier' => null,
|
||||||
|
'metadataPrefix' => null,
|
||||||
|
'from' => null,
|
||||||
|
'until' => null,
|
||||||
|
'set' => null,
|
||||||
|
'resumptionToken' => null,
|
||||||
|
'counter' => 0,
|
||||||
|
'completeListSize' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the entity manager singleton.
|
||||||
|
*/
|
||||||
|
protected EntityManager $em;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the prepared response document.
|
* This holds the prepared response document.
|
||||||
*/
|
*/
|
||||||
protected Document $preparedResponse;
|
protected Response $preparedResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add resumption token information to response document.
|
||||||
|
*
|
||||||
|
* @param DOMElement $node The DOM node to add the resumption token to
|
||||||
|
* @param ?Token $token The new resumption token or NULL if none
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function addResumptionToken(DOMElement $node, ?Token $token): void
|
||||||
|
{
|
||||||
|
if (isset($token) || isset($this->arguments['resumptionToken'])) {
|
||||||
|
$resumptionToken = $this->preparedResponse->createElement(localName: 'resumptionToken');
|
||||||
|
if (isset($token)) {
|
||||||
|
$resumptionToken->nodeValue = $token->getToken();
|
||||||
|
$resumptionToken->setAttribute(
|
||||||
|
qualifiedName: 'expirationDate',
|
||||||
|
value: $token->getValidUntil()->format(format: 'Y-m-d\TH:i:s\Z')
|
||||||
|
);
|
||||||
|
$this->arguments['completeListSize'] = $token->getParameters()['completeListSize'];
|
||||||
|
}
|
||||||
|
$resumptionToken->setAttribute(
|
||||||
|
qualifiedName: 'completeListSize',
|
||||||
|
value: (string) $this->arguments['completeListSize']
|
||||||
|
);
|
||||||
|
$resumptionToken->setAttribute(
|
||||||
|
qualifiedName: 'cursor',
|
||||||
|
value: (string) ($this->arguments['counter'] * Configuration::getInstance()->maxRecords)
|
||||||
|
);
|
||||||
|
$node->appendChild(node: $resumptionToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for resumption token and populate request arguments.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function checkResumptionToken(): void
|
||||||
|
{
|
||||||
|
if (isset($this->arguments['resumptionToken'])) {
|
||||||
|
$token = $this->em->getResumptionToken(
|
||||||
|
token: $this->arguments['resumptionToken'],
|
||||||
|
verb: $this->arguments['verb']
|
||||||
|
);
|
||||||
|
if (isset($token)) {
|
||||||
|
$this->arguments = array_merge($this->arguments, $token->getParameters());
|
||||||
|
} else {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'badResumptionToken');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare response document.
|
* Prepare response document.
|
||||||
|
@ -59,7 +147,10 @@ abstract class Middleware extends AbstractMiddleware
|
||||||
*/
|
*/
|
||||||
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
||||||
{
|
{
|
||||||
$this->prepareResponse($request);
|
/** @var OaiRequestMetadata */
|
||||||
|
$arguments = $request->getAttributes();
|
||||||
|
$this->arguments = array_merge($this->arguments, $arguments);
|
||||||
|
$this->prepareResponse(request: $request);
|
||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,18 +164,22 @@ abstract class Middleware extends AbstractMiddleware
|
||||||
protected function processResponse(ResponseInterface $response): ResponseInterface
|
protected function processResponse(ResponseInterface $response): ResponseInterface
|
||||||
{
|
{
|
||||||
if (!ErrorHandler::getInstance()->hasErrors() && isset($this->preparedResponse)) {
|
if (!ErrorHandler::getInstance()->hasErrors() && isset($this->preparedResponse)) {
|
||||||
$response = $response->withBody(Utils::streamFor((string) $this->preparedResponse));
|
$response = $response->withBody(
|
||||||
|
body: Utils::streamFor(
|
||||||
|
resource: (string) $this->preparedResponse
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The constructor must have the same signature for all derived classes, thus make it final.
|
* The constructor must have the same signature for all derived classes, thus make it final.
|
||||||
|
*
|
||||||
|
* @see https://psalm.dev/229
|
||||||
*/
|
*/
|
||||||
final public function __construct()
|
final public function __construct()
|
||||||
{
|
{
|
||||||
// Make constructor final to avoid issues in dispatcher.
|
$this->em = EntityManager::getInstance();
|
||||||
// @see https://psalm.dev/229
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Middleware;
|
namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\EntityManager;
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
use OCC\PSR15\AbstractMiddleware;
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
@ -37,6 +38,8 @@ class Dispatcher extends AbstractMiddleware
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* List of defined OAI-PMH parameters.
|
* List of defined OAI-PMH parameters.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected const OAI_PARAMS = [
|
protected const OAI_PARAMS = [
|
||||||
'verb',
|
'verb',
|
||||||
|
@ -62,14 +65,14 @@ class Dispatcher extends AbstractMiddleware
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
$arguments = $request->getQueryParams();
|
$arguments = $request->getQueryParams();
|
||||||
} elseif ($request->getMethod() === 'POST') {
|
} elseif ($request->getMethod() === 'POST') {
|
||||||
if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
|
if ($request->getHeaderLine(name: 'Content-Type') === 'application/x-www-form-urlencoded') {
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
$arguments = (array) $request->getParsedBody();
|
$arguments = (array) $request->getParsedBody();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->validateArguments($arguments)) {
|
if ($this->validateArguments(arguments: $arguments)) {
|
||||||
foreach ($arguments as $param => $value) {
|
foreach ($arguments as $param => $value) {
|
||||||
$request = $request->withAttribute($param, $value);
|
$request = $request->withAttribute(name: $param, value: $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $request;
|
return $request;
|
||||||
|
@ -84,17 +87,17 @@ class Dispatcher extends AbstractMiddleware
|
||||||
*/
|
*/
|
||||||
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
||||||
{
|
{
|
||||||
$request = $this->getRequestWithAttributes($request);
|
$request = $this->getRequestWithAttributes(request: $request);
|
||||||
$errorHandler = ErrorHandler::getInstance();
|
$errorHandler = ErrorHandler::getInstance();
|
||||||
if (!$errorHandler->hasErrors()) {
|
if (!$errorHandler->hasErrors()) {
|
||||||
/** @var string */
|
/** @var string */
|
||||||
$verb = $request->getAttribute('verb');
|
$verb = $request->getAttribute(name: 'verb');
|
||||||
$middleware = __NAMESPACE__ . '\\' . $verb;
|
$middleware = __NAMESPACE__ . '\\' . $verb;
|
||||||
if (is_a($middleware, Middleware::class, true)) {
|
if (is_a(object_or_class: $middleware, class: Middleware::class, allow_string: true)) {
|
||||||
$this->requestHandler->queue->enqueue(new $middleware());
|
$this->requestHandler->queue->enqueue(value: new $middleware());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->requestHandler->queue->enqueue($errorHandler);
|
$this->requestHandler->queue->enqueue(value: $errorHandler);
|
||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +113,7 @@ class Dispatcher extends AbstractMiddleware
|
||||||
// TODO: Add support for content compression
|
// TODO: Add support for content compression
|
||||||
// https://openarchives.org/OAI/openarchivesprotocol.html#ResponseCompression
|
// https://openarchives.org/OAI/openarchivesprotocol.html#ResponseCompression
|
||||||
// https://github.com/middlewares/encoder
|
// https://github.com/middlewares/encoder
|
||||||
return $response->withHeader('Content-Type', 'text/xml');
|
return $response->withHeader(name: 'Content-Type', value: 'text/xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,30 +127,104 @@ class Dispatcher extends AbstractMiddleware
|
||||||
*/
|
*/
|
||||||
protected function validateArguments(array $arguments): bool
|
protected function validateArguments(array $arguments): bool
|
||||||
{
|
{
|
||||||
$errorHandler = ErrorHandler::getInstance();
|
|
||||||
if (
|
if (
|
||||||
count(array_diff(array_keys($arguments), self::OAI_PARAMS)) !== 0
|
count(array_diff(array_keys($arguments), self::OAI_PARAMS)) !== 0
|
||||||
or !isset($arguments['verb'])
|
or !isset($arguments['verb'])
|
||||||
) {
|
) {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
} else {
|
} else {
|
||||||
switch ($arguments['verb']) {
|
match ($arguments['verb']) {
|
||||||
case 'GetRecord':
|
'GetRecord' => $this->validateGetRecord(arguments: $arguments),
|
||||||
|
'Identify' => $this->validateIdentify(arguments: $arguments),
|
||||||
|
'ListIdentifiers', 'ListRecords' => $this->validateListRecords(arguments: $arguments),
|
||||||
|
'ListMetadataFormats' => $this->validateListFormats(arguments: $arguments),
|
||||||
|
'ListSets' => $this->validateListSets(arguments: $arguments),
|
||||||
|
default => ErrorHandler::getInstance()->withError(errorCode: 'badVerb')
|
||||||
|
};
|
||||||
|
if (!ErrorHandler::getInstance()->hasErrors()) {
|
||||||
|
$this->validateMetadataPrefix(prefix: $arguments['metadataPrefix'] ?? null);
|
||||||
|
$this->validateDateTime(datetime: $arguments['from'] ?? null);
|
||||||
|
$this->validateDateTime(datetime: $arguments['until'] ?? null);
|
||||||
|
$this->validateSet($arguments['set'] ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !ErrorHandler::getInstance()->hasErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate "from" and "until" argument.
|
||||||
|
*
|
||||||
|
* @param ?string $datetime The datetime string to validate or NULL if none
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateDateTime(?string $datetime): void
|
||||||
|
{
|
||||||
|
if (isset($datetime)) {
|
||||||
|
$date = date_parse(datetime: $datetime);
|
||||||
|
if ($date['warning_count'] > 0 || $date['error_count'] > 0) {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate request arguments for verb GetRecord.
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateGetRecord(array $arguments): void
|
||||||
|
{
|
||||||
if (
|
if (
|
||||||
count($arguments) !== 3
|
count($arguments) !== 3
|
||||||
or !isset($arguments['identifier'])
|
or !isset($arguments['identifier'])
|
||||||
or !isset($arguments['metadataPrefix'])
|
or !isset($arguments['metadataPrefix'])
|
||||||
) {
|
) {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case 'Identify':
|
|
||||||
|
/**
|
||||||
|
* Validate request arguments for verb Identify.
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateIdentify(array $arguments): void
|
||||||
|
{
|
||||||
if (count($arguments) !== 1) {
|
if (count($arguments) !== 1) {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case 'ListIdentifiers':
|
|
||||||
case 'ListRecords':
|
/**
|
||||||
|
* Validate request arguments for verb ListMetadataFormats.
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateListFormats(array $arguments): void
|
||||||
|
{
|
||||||
|
if (count($arguments) !== 1) {
|
||||||
|
if (!isset($arguments['identifier']) || count($arguments) !== 2) {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate request arguments for verbs ListIdentifiers and ListRecords.
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateListRecords(array $arguments): void
|
||||||
|
{
|
||||||
if (
|
if (
|
||||||
isset($arguments['metadataPrefix'])
|
isset($arguments['metadataPrefix'])
|
||||||
xor isset($arguments['resumptionToken'])
|
xor isset($arguments['resumptionToken'])
|
||||||
|
@ -156,30 +233,60 @@ class Dispatcher extends AbstractMiddleware
|
||||||
(isset($arguments['resumptionToken']) && count($arguments) !== 2)
|
(isset($arguments['resumptionToken']) && count($arguments) !== 2)
|
||||||
or isset($arguments['identifier'])
|
or isset($arguments['identifier'])
|
||||||
) {
|
) {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ListMetadataFormats':
|
|
||||||
if (count($arguments) !== 1) {
|
|
||||||
if (!isset($arguments['identifier']) || count($arguments) !== 2) {
|
|
||||||
$errorHandler->withError('badArgument');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case 'ListSets':
|
/**
|
||||||
|
* Validate request arguments for verb ListSets.
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateListSets(array $arguments): void
|
||||||
|
{
|
||||||
if (count($arguments) !== 1) {
|
if (count($arguments) !== 1) {
|
||||||
if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) {
|
if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) {
|
||||||
$errorHandler->withError('badArgument');
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
|
||||||
$errorHandler->withError('badVerb');
|
/**
|
||||||
|
* Validate "metadataPrefix" argument.
|
||||||
|
*
|
||||||
|
* @param ?string $prefix The metadata prefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateMetadataPrefix(?string $prefix): void
|
||||||
|
{
|
||||||
|
if (isset($prefix)) {
|
||||||
|
$formats = EntityManager::getInstance()->getMetadataFormats();
|
||||||
|
if (!$formats->containsKey(key: $prefix)) {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'cannotDisseminateFormat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate "set" argument.
|
||||||
|
*
|
||||||
|
* @param ?string $spec The set spec
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function validateSet(?string $spec): void
|
||||||
|
{
|
||||||
|
if (isset($spec)) {
|
||||||
|
$sets = EntityManager::getInstance()->getSets();
|
||||||
|
if (!$sets->containsKey(key: $spec)) {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'badArgument');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return !$errorHandler->hasErrors();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ namespace OCC\OaiPmh2\Middleware;
|
||||||
use DomainException;
|
use DomainException;
|
||||||
use GuzzleHttp\Psr7\Utils;
|
use GuzzleHttp\Psr7\Utils;
|
||||||
use OCC\Basics\Traits\Singleton;
|
use OCC\Basics\Traits\Singleton;
|
||||||
use OCC\OaiPmh2\Document;
|
use OCC\OaiPmh2\Response;
|
||||||
use OCC\PSR15\AbstractMiddleware;
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\StreamInterface;
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
@ -48,7 +48,7 @@ class ErrorHandler extends AbstractMiddleware
|
||||||
protected const OAI_ERRORS = [
|
protected const OAI_ERRORS = [
|
||||||
'badArgument' => 'The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.',
|
'badArgument' => 'The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.',
|
||||||
'badResumptionToken' => 'The value of the resumptionToken argument is invalid or expired.',
|
'badResumptionToken' => 'The value of the resumptionToken argument is invalid or expired.',
|
||||||
'badVerb' => 'Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated.',
|
'badVerb' => 'The value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated.',
|
||||||
'cannotDisseminateFormat' => 'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.',
|
'cannotDisseminateFormat' => 'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.',
|
||||||
'idDoesNotExist' => 'The value of the identifier argument is unknown or illegal in this repository.',
|
'idDoesNotExist' => 'The value of the identifier argument is unknown or illegal in this repository.',
|
||||||
'noRecordsMatch' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.',
|
'noRecordsMatch' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.',
|
||||||
|
@ -70,12 +70,19 @@ class ErrorHandler extends AbstractMiddleware
|
||||||
*/
|
*/
|
||||||
protected function getResponseBody(): StreamInterface
|
protected function getResponseBody(): StreamInterface
|
||||||
{
|
{
|
||||||
$document = new Document($this->requestHandler->request);
|
$response = new Response(serverRequest: $this->requestHandler->request);
|
||||||
foreach (array_unique($this->errors) as $errorCode) {
|
foreach (array_unique($this->errors) as $errorCode) {
|
||||||
$error = $document->createElement('error', self::OAI_ERRORS[$errorCode], true);
|
$error = $response->createElement(
|
||||||
$error->setAttribute('code', $errorCode);
|
localName: 'error',
|
||||||
|
value: self::OAI_ERRORS[$errorCode],
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
|
$error->setAttribute(
|
||||||
|
qualifiedName: 'code',
|
||||||
|
value: $errorCode
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Utils::streamFor((string) $document);
|
return Utils::streamFor(resource: (string) $response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,7 +105,7 @@ class ErrorHandler extends AbstractMiddleware
|
||||||
protected function processResponse(ResponseInterface $response): ResponseInterface
|
protected function processResponse(ResponseInterface $response): ResponseInterface
|
||||||
{
|
{
|
||||||
if ($this->hasErrors()) {
|
if ($this->hasErrors()) {
|
||||||
$response = $response->withBody($this->getResponseBody());
|
$response = $response->withBody(body: $this->getResponseBody());
|
||||||
}
|
}
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
@ -118,11 +125,11 @@ class ErrorHandler extends AbstractMiddleware
|
||||||
$this->errors[] = $errorCode;
|
$this->errors[] = $errorCode;
|
||||||
} else {
|
} else {
|
||||||
throw new DomainException(
|
throw new DomainException(
|
||||||
sprintf(
|
message: sprintf(
|
||||||
'Valid OAI-PMH error code expected, "%s" given.',
|
format: 'Valid OAI-PMH error code expected, "%s" given.',
|
||||||
$errorCode
|
values: $errorCode
|
||||||
),
|
),
|
||||||
500
|
code: 500
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
@ -22,14 +22,13 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Middleware;
|
namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Document;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\OaiPmh2\Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "GetRecord" request.
|
* Process the "GetRecord" request.
|
||||||
|
*
|
||||||
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#GetRecord
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#GetRecord
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
@ -46,54 +45,67 @@ class GetRecord extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function prepareResponse(ServerRequestInterface $request): void
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
$oaiRecord = $this->em->getRecord(
|
||||||
$params = $request->getAttributes();
|
identifier: (string) $this->arguments['identifier'],
|
||||||
/** @var Format */
|
format: (string) $this->arguments['metadataPrefix']
|
||||||
$format = Database::getInstance()->getEntityManager()->getReference(Format::class, $params['metadataPrefix']);
|
);
|
||||||
$oaiRecord = Database::getInstance()->getRecord($params['identifier'], $format);
|
|
||||||
|
|
||||||
if (!isset($oaiRecord)) {
|
if (!isset($oaiRecord)) {
|
||||||
if (Database::getInstance()->idDoesExist($params['identifier'])) {
|
if ($this->em->isValidRecordIdentifier(identifier: (string) $this->arguments['identifier'])) {
|
||||||
ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
|
ErrorHandler::getInstance()->withError(errorCode: 'cannotDisseminateFormat');
|
||||||
} else {
|
} else {
|
||||||
ErrorHandler::getInstance()->withError('idDoesNotExist');
|
ErrorHandler::getInstance()->withError(errorCode: 'idDoesNotExist');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
$oaiRecordContent = $oaiRecord->getContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$document = new Document($request);
|
$response = new Response(serverRequest: $request);
|
||||||
$getRecord = $document->createElement('GetRecord', '', true);
|
$getRecord = $response->createElement(
|
||||||
|
localName: 'GetRecord',
|
||||||
|
value: '',
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
|
|
||||||
$record = $document->createElement('record');
|
$record = $response->createElement(localName: 'record');
|
||||||
$getRecord->appendChild($record);
|
$getRecord->appendChild(node: $record);
|
||||||
|
|
||||||
$header = $document->createElement('header');
|
$header = $response->createElement(localName: 'header');
|
||||||
if (!isset($oaiRecordContent)) {
|
if (!$oaiRecord->hasContent()) {
|
||||||
$header->setAttribute('status', 'deleted');
|
$header->setAttribute(
|
||||||
|
qualifiedName: 'status',
|
||||||
|
value: 'deleted'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
$record->appendChild($header);
|
$record->appendChild(node: $header);
|
||||||
|
|
||||||
$identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
|
$identifier = $response->createElement(
|
||||||
$header->appendChild($identifier);
|
localName: 'identifier',
|
||||||
|
value: $oaiRecord->getIdentifier()
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $identifier);
|
||||||
|
|
||||||
$datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
|
$datestamp = $response->createElement(
|
||||||
$header->appendChild($datestamp);
|
localName: 'datestamp',
|
||||||
|
value: $oaiRecord->getLastChanged()->format(format: 'Y-m-d\TH:i:s\Z')
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $datestamp);
|
||||||
|
|
||||||
foreach ($oaiRecord->getSets() as $set) {
|
foreach ($oaiRecord->getSets() as $set) {
|
||||||
$setSpec = $document->createElement('setSpec', $set->getName());
|
$setSpec = $response->createElement(
|
||||||
$header->appendChild($setSpec);
|
localName: 'setSpec',
|
||||||
|
value: $set->getName()
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $setSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($oaiRecordContent)) {
|
if ($oaiRecord->hasContent()) {
|
||||||
$metadata = $document->createElement('metadata');
|
$metadata = $response->createElement(localName: 'metadata');
|
||||||
$record->appendChild($metadata);
|
$record->appendChild(node: $metadata);
|
||||||
|
|
||||||
$data = $document->importData($oaiRecordContent);
|
$data = $response->importData(data: $oaiRecord->getContent());
|
||||||
$metadata->appendChild($data);
|
$metadata->appendChild(node: $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->preparedResponse = $document;
|
$this->preparedResponse = $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,13 @@ namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
use GuzzleHttp\Psr7\Uri;
|
use GuzzleHttp\Psr7\Uri;
|
||||||
use OCC\OaiPmh2\Configuration;
|
use OCC\OaiPmh2\Configuration;
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Document;
|
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\OaiPmh2\Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "Identify" request.
|
* Process the "Identify" request.
|
||||||
|
*
|
||||||
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
@ -47,47 +47,75 @@ class Identify extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function prepareResponse(ServerRequestInterface $request): void
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
{
|
{
|
||||||
$document = new Document($request);
|
$response = new Response(serverRequest: $request);
|
||||||
$identify = $document->createElement('Identify', '', true);
|
$identify = $response->createElement(
|
||||||
|
localName: 'Identify',
|
||||||
|
value: '',
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
|
|
||||||
$name = Configuration::getInstance()->repositoryName;
|
$repositoryName = $response->createElement(
|
||||||
$repositoryName = $document->createElement('repositoryName', $name);
|
localName: 'repositoryName',
|
||||||
$identify->appendChild($repositoryName);
|
value: Configuration::getInstance()->repositoryName
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $repositoryName);
|
||||||
|
|
||||||
$uri = Uri::composeComponents(
|
$uri = Uri::composeComponents(
|
||||||
$request->getUri()->getScheme(),
|
scheme: $request->getUri()->getScheme(),
|
||||||
$request->getUri()->getAuthority(),
|
authority: $request->getUri()->getAuthority(),
|
||||||
$request->getUri()->getPath(),
|
path: $request->getUri()->getPath(),
|
||||||
null,
|
query: null,
|
||||||
null
|
fragment: null
|
||||||
);
|
);
|
||||||
$baseURL = $document->createElement('baseURL', $uri);
|
$baseURL = $response->createElement(
|
||||||
$identify->appendChild($baseURL);
|
localName: 'baseURL',
|
||||||
|
value: $uri
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $baseURL);
|
||||||
|
|
||||||
$protocolVersion = $document->createElement('protocolVersion', '2.0');
|
$protocolVersion = $response->createElement(
|
||||||
$identify->appendChild($protocolVersion);
|
localName: 'protocolVersion',
|
||||||
|
value: '2.0'
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $protocolVersion);
|
||||||
|
|
||||||
$email = Configuration::getInstance()->adminEmail;
|
$adminEmail = $response->createElement(
|
||||||
$adminEmail = $document->createElement('adminEmail', $email);
|
localName: 'adminEmail',
|
||||||
$identify->appendChild($adminEmail);
|
value: Configuration::getInstance()->adminEmail
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $adminEmail);
|
||||||
|
|
||||||
$datestamp = Database::getInstance()->getEarliestDatestamp();
|
$earliestDatestamp = $response->createElement(
|
||||||
$earliestDatestamp = $document->createElement('earliestDatestamp', $datestamp);
|
localName: 'earliestDatestamp',
|
||||||
$identify->appendChild($earliestDatestamp);
|
value: $this->em->getEarliestDatestamp()
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $earliestDatestamp);
|
||||||
|
|
||||||
$deletedRecord = $document->createElement('deletedRecord', 'transient');
|
$deletedRecord = $response->createElement(
|
||||||
$identify->appendChild($deletedRecord);
|
localName: 'deletedRecord',
|
||||||
|
value: Configuration::getInstance()->deletedRecords
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $deletedRecord);
|
||||||
|
|
||||||
$granularity = $document->createElement('granularity', 'YYYY-MM-DDThh:mm:ssZ');
|
$granularity = $response->createElement(
|
||||||
$identify->appendChild($granularity);
|
localName: 'granularity',
|
||||||
|
value: 'YYYY-MM-DDThh:mm:ssZ'
|
||||||
|
);
|
||||||
|
$identify->appendChild(node: $granularity);
|
||||||
|
|
||||||
// TODO: Implement explicit content compression support.
|
// TODO: Implement explicit content compression support.
|
||||||
// $compressionDeflate = $document->createElement('compression', 'deflate');
|
// $compressionDeflate = $response->createElement(
|
||||||
// $identify->appendChild($compressionDeflate);
|
// localName: 'compression',
|
||||||
|
// value: 'deflate'
|
||||||
|
// );
|
||||||
|
// $identify->appendChild(node: $compressionDeflate);
|
||||||
|
|
||||||
// $compressionGzip = $document->createElement('compression', 'gzip');
|
// $compressionGzip = $response->createElement(
|
||||||
// $identify->appendChild($compressionGzip);
|
// localName: 'compression',
|
||||||
|
// value: 'gzip'
|
||||||
|
// );
|
||||||
|
// $identify->appendChild(node: $compressionGzip);
|
||||||
|
|
||||||
$this->preparedResponse = $document;
|
$this->preparedResponse = $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,13 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Middleware;
|
namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
use DateTime;
|
|
||||||
use OCC\OaiPmh2\Configuration;
|
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Document;
|
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\OaiPmh2\Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "ListIdentifiers" request.
|
* Process the "ListIdentifiers" request.
|
||||||
|
*
|
||||||
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListIdentifiers
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListIdentifiers
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
@ -40,7 +37,7 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||||
class ListIdentifiers extends Middleware
|
class ListIdentifiers extends Middleware
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Prepare the response body for verb "ListIdentifiers" and "ListRecords".
|
* Prepare the response body for verbs "ListIdentifiers" and "ListRecords".
|
||||||
*
|
*
|
||||||
* @param ServerRequestInterface $request The incoming request
|
* @param ServerRequestInterface $request The incoming request
|
||||||
*
|
*
|
||||||
|
@ -48,127 +45,78 @@ class ListIdentifiers extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function prepareResponse(ServerRequestInterface $request): void
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
{
|
{
|
||||||
$counter = 0;
|
$this->checkResumptionToken();
|
||||||
$completeListSize = 0;
|
|
||||||
$maxRecords = Configuration::getInstance()->maxRecords;
|
|
||||||
|
|
||||||
/** @var array<string, string> */
|
$records = $this->em->getRecords(
|
||||||
$params = $request->getAttributes();
|
verb: $this->arguments['verb'],
|
||||||
$verb = $params['verb'];
|
metadataPrefix: (string) $this->arguments['metadataPrefix'],
|
||||||
$metadataPrefix = $params['metadataPrefix'] ?? '';
|
counter: $this->arguments['counter'],
|
||||||
$from = $params['from'] ?? null;
|
from: $this->arguments['from'],
|
||||||
$until = $params['until'] ?? null;
|
until: $this->arguments['until'],
|
||||||
$set = $params['set'] ?? null;
|
set: $this->arguments['set']
|
||||||
$resumptionToken = $params['resumptionToken'] ?? null;
|
|
||||||
|
|
||||||
if (isset($resumptionToken)) {
|
|
||||||
$oldToken = Database::getInstance()->getResumptionToken($resumptionToken, $verb);
|
|
||||||
if (!isset($oldToken)) {
|
|
||||||
ErrorHandler::getInstance()->withError('badResumptionToken');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
foreach ($oldToken->getParameters() as $key => $value) {
|
|
||||||
$$key = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$prefixes = Database::getInstance()->getMetadataFormats()->getQueryResult();
|
|
||||||
if (!array_key_exists($metadataPrefix, $prefixes)) {
|
|
||||||
ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isset($from)) {
|
|
||||||
$from = new DateTime($from);
|
|
||||||
}
|
|
||||||
if (isset($until)) {
|
|
||||||
$until = new DateTime($until);
|
|
||||||
}
|
|
||||||
if (isset($set)) {
|
|
||||||
$sets = Database::getInstance()->getSets()->getQueryResult();
|
|
||||||
if (!array_key_exists($set, $sets)) {
|
|
||||||
ErrorHandler::getInstance()->withError('noSetHierarchy');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$set = $sets[$set];
|
|
||||||
}
|
|
||||||
|
|
||||||
$records = Database::getInstance()->getRecords(
|
|
||||||
$verb,
|
|
||||||
$prefixes[$metadataPrefix],
|
|
||||||
$counter,
|
|
||||||
$from,
|
|
||||||
$until,
|
|
||||||
$set
|
|
||||||
);
|
);
|
||||||
$newToken = $records->getResumptionToken();
|
|
||||||
if (count($records) === 0) {
|
if (count($records) === 0) {
|
||||||
ErrorHandler::getInstance()->withError('noRecordsMatch');
|
ErrorHandler::getInstance()->withError(errorCode: 'noRecordsMatch');
|
||||||
return;
|
return;
|
||||||
} elseif (isset($newToken)) {
|
|
||||||
$completeListSize = $newToken->getParameters()['completeListSize'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$document = new Document($request);
|
$response = new Response(serverRequest: $request);
|
||||||
$list = $document->createElement($verb, '', true);
|
$list = $response->createElement(
|
||||||
|
localName: $this->arguments['verb'],
|
||||||
/** @var Record $oaiRecord */
|
value: '',
|
||||||
foreach ($records as $oaiRecord) {
|
appendToRoot: true
|
||||||
if ($verb === 'ListIdentifiers') {
|
);
|
||||||
$baseNode = $list;
|
$baseNode = $list;
|
||||||
} else {
|
|
||||||
$record = $document->createElement('record');
|
foreach ($records as $oaiRecord) {
|
||||||
$list->appendChild($record);
|
if ($this->arguments['verb'] === 'ListRecords') {
|
||||||
|
$record = $response->createElement(localName: 'record');
|
||||||
|
$list->appendChild(node: $record);
|
||||||
$baseNode = $record;
|
$baseNode = $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
$header = $document->createElement('header');
|
$header = $response->createElement(localName: 'header');
|
||||||
if (!$oaiRecord->hasContent()) {
|
$baseNode->appendChild(node: $header);
|
||||||
$header->setAttribute('status', 'deleted');
|
|
||||||
}
|
|
||||||
$baseNode->appendChild($header);
|
|
||||||
|
|
||||||
$identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
|
$identifier = $response->createElement(
|
||||||
$header->appendChild($identifier);
|
localName: 'identifier',
|
||||||
|
value: $oaiRecord->getIdentifier()
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $identifier);
|
||||||
|
|
||||||
$datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
|
$datestamp = $response->createElement(
|
||||||
$header->appendChild($datestamp);
|
localName: 'datestamp',
|
||||||
|
value: $oaiRecord->getLastChanged()->format(format: 'Y-m-d\TH:i:s\Z')
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $datestamp);
|
||||||
|
|
||||||
foreach ($oaiRecord->getSets() as $oaiSet) {
|
foreach ($oaiRecord->getSets() as $oaiSet) {
|
||||||
$setSpec = $document->createElement('setSpec', $oaiSet->getName());
|
$setSpec = $response->createElement(
|
||||||
$header->appendChild($setSpec);
|
localName: 'setSpec',
|
||||||
|
value: $oaiSet->getName()
|
||||||
|
);
|
||||||
|
$header->appendChild(node: $setSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($verb === 'ListRecords' && $oaiRecord->hasContent()) {
|
if (!$oaiRecord->hasContent()) {
|
||||||
$metadata = $document->createElement('metadata');
|
$header->setAttribute(
|
||||||
$baseNode->appendChild($metadata);
|
qualifiedName: 'status',
|
||||||
|
value: 'deleted'
|
||||||
|
);
|
||||||
|
} elseif ($this->arguments['verb'] === 'ListRecords') {
|
||||||
|
$metadata = $response->createElement(localName: 'metadata');
|
||||||
|
$baseNode->appendChild(node: $metadata);
|
||||||
|
|
||||||
/** @var string */
|
$data = $response->importData(data: $oaiRecord->getContent());
|
||||||
$content = $oaiRecord->getContent();
|
$metadata->appendChild(node: $data);
|
||||||
$data = $document->importData($content);
|
|
||||||
$metadata->appendChild($data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($oldToken) || isset($newToken)) {
|
$this->preparedResponse = $response;
|
||||||
$resumptionToken = $document->createElement('resumptionToken');
|
|
||||||
$list->appendChild($resumptionToken);
|
$this->addResumptionToken(
|
||||||
if (isset($newToken)) {
|
node: $list,
|
||||||
$resumptionToken->nodeValue = $newToken->getToken();
|
token: $records->getResumptionToken() ?? null
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'expirationDate',
|
|
||||||
$newToken->getValidUntil()->format('Y-m-d\TH:i:s\Z')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'completeListSize',
|
|
||||||
(string) $completeListSize
|
|
||||||
);
|
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'cursor',
|
|
||||||
(string) ($counter * $maxRecords)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->preparedResponse = $document;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,14 +22,13 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Middleware;
|
namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Document;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\OaiPmh2\Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "ListMetadataFormats" request.
|
* Process the "ListMetadataFormats" request.
|
||||||
|
*
|
||||||
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListMetadataFormats
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListMetadataFormats
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
@ -46,37 +45,50 @@ class ListMetadataFormats extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function prepareResponse(ServerRequestInterface $request): void
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
{
|
{
|
||||||
/** @var ?string */
|
$formats = $this->em->getMetadataFormats(recordIdentifier: $this->arguments['identifier']);
|
||||||
$identifier = $request->getAttribute('identifier');
|
|
||||||
$formats = Database::getInstance()->getMetadataFormats($identifier);
|
|
||||||
|
|
||||||
if (count($formats) === 0) {
|
if (count($formats) === 0) {
|
||||||
if (!isset($identifier) || Database::getInstance()->idDoesExist($identifier)) {
|
if (
|
||||||
ErrorHandler::getInstance()->withError('noMetadataFormats');
|
!isset($this->arguments['identifier'])
|
||||||
|
|| $this->em->isValidRecordIdentifier(identifier: $this->arguments['identifier'])
|
||||||
|
) {
|
||||||
|
ErrorHandler::getInstance()->withError(errorCode: 'noMetadataFormats');
|
||||||
} else {
|
} else {
|
||||||
ErrorHandler::getInstance()->withError('idDoesNotExist');
|
ErrorHandler::getInstance()->withError(errorCode: 'idDoesNotExist');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$document = new Document($request);
|
$response = new Response(serverRequest: $request);
|
||||||
$listMetadataFormats = $document->createElement('ListMetadataFormats', '', true);
|
$listMetadataFormats = $response->createElement(
|
||||||
|
localName: 'ListMetadataFormats',
|
||||||
|
value: '',
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
|
|
||||||
/** @var Format $oaiFormat */
|
|
||||||
foreach ($formats as $oaiFormat) {
|
foreach ($formats as $oaiFormat) {
|
||||||
$metadataFormat = $document->createElement('metadataFormat');
|
$metadataFormat = $response->createElement(localName: 'metadataFormat');
|
||||||
$listMetadataFormats->appendChild($metadataFormat);
|
$listMetadataFormats->appendChild(node: $metadataFormat);
|
||||||
|
|
||||||
$metadataPrefix = $document->createElement('metadataPrefix', $oaiFormat->getPrefix());
|
$metadataPrefix = $response->createElement(
|
||||||
$metadataFormat->appendChild($metadataPrefix);
|
localName: 'metadataPrefix',
|
||||||
|
value: $oaiFormat->getPrefix()
|
||||||
|
);
|
||||||
|
$metadataFormat->appendChild(node: $metadataPrefix);
|
||||||
|
|
||||||
$schema = $document->createElement('schema', $oaiFormat->getSchema());
|
$schema = $response->createElement(
|
||||||
$metadataFormat->appendChild($schema);
|
localName: 'schema',
|
||||||
|
value: $oaiFormat->getSchema()
|
||||||
|
);
|
||||||
|
$metadataFormat->appendChild(node: $schema);
|
||||||
|
|
||||||
$metadataNamespace = $document->createElement('metadataNamespace', $oaiFormat->getNamespace());
|
$metadataNamespace = $response->createElement(
|
||||||
$metadataFormat->appendChild($metadataNamespace);
|
localName: 'metadataNamespace',
|
||||||
|
value: $oaiFormat->getNamespace()
|
||||||
|
);
|
||||||
|
$metadataFormat->appendChild(node: $metadataNamespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->preparedResponse = $document;
|
$this->preparedResponse = $response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "ListRecords" request.
|
* Process the "ListRecords" request.
|
||||||
|
*
|
||||||
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListRecords
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListRecords
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
@ -34,6 +35,6 @@ class ListRecords extends ListIdentifiers
|
||||||
/**
|
/**
|
||||||
* "ListIdentifiers" and "ListRecords" are practically identical except the
|
* "ListIdentifiers" and "ListRecords" are practically identical except the
|
||||||
* former returns the header information only while the latter also returns
|
* former returns the header information only while the latter also returns
|
||||||
* the records' data.
|
* the records' data. Hence this is just a class alias.
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,21 +22,17 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2\Middleware;
|
namespace OCC\OaiPmh2\Middleware;
|
||||||
|
|
||||||
use OCC\OaiPmh2\Configuration;
|
|
||||||
use OCC\OaiPmh2\Database;
|
|
||||||
use OCC\OaiPmh2\Document;
|
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
|
||||||
use OCC\OaiPmh2\Middleware;
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\OaiPmh2\Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the "ListSets" request.
|
* Process the "ListSets" request.
|
||||||
|
*
|
||||||
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ListSets
|
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ListSets
|
||||||
*
|
*
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*
|
|
||||||
* @template Sets of array<string, Set>
|
|
||||||
*/
|
*/
|
||||||
class ListSets extends Middleware
|
class ListSets extends Middleware
|
||||||
{
|
{
|
||||||
|
@ -49,78 +45,52 @@ class ListSets extends Middleware
|
||||||
*/
|
*/
|
||||||
protected function prepareResponse(ServerRequestInterface $request): void
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
{
|
{
|
||||||
$counter = 0;
|
$this->checkResumptionToken();
|
||||||
$completeListSize = 0;
|
|
||||||
$maxRecords = Configuration::getInstance()->maxRecords;
|
|
||||||
|
|
||||||
/** @var ?string */
|
$sets = $this->em->getSets(counter: $this->arguments['counter']);
|
||||||
$token = $request->getAttribute('resumptionToken');
|
|
||||||
if (isset($token)) {
|
|
||||||
$oldToken = Database::getInstance()->getResumptionToken($token, 'ListSets');
|
|
||||||
if (!isset($oldToken)) {
|
|
||||||
ErrorHandler::getInstance()->withError('badResumptionToken');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
foreach ($oldToken->getParameters() as $key => $value) {
|
|
||||||
$$key = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$sets = Database::getInstance()->getSets($counter);
|
|
||||||
$newToken = $sets->getResumptionToken();
|
|
||||||
if (count($sets) === 0) {
|
if (count($sets) === 0) {
|
||||||
ErrorHandler::getInstance()->withError('noSetHierarchy');
|
ErrorHandler::getInstance()->withError(errorCode: 'noSetHierarchy');
|
||||||
return;
|
return;
|
||||||
} elseif (isset($newToken)) {
|
|
||||||
$completeListSize = $newToken->getParameters()['completeListSize'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$document = new Document($request);
|
$response = new Response(serverRequest: $request);
|
||||||
$list = $document->createElement('ListSets', '', true);
|
$list = $response->createElement(
|
||||||
|
localName: 'ListSets',
|
||||||
|
value: '',
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
|
|
||||||
/** @var Set $oaiSet */
|
|
||||||
foreach ($sets as $oaiSet) {
|
foreach ($sets as $oaiSet) {
|
||||||
$set = $document->createElement('set');
|
$set = $response->createElement(localName: 'set');
|
||||||
$list->appendChild($set);
|
$list->appendChild(node: $set);
|
||||||
|
|
||||||
$setSpec = $document->createElement('setSpec', $oaiSet->getSpec());
|
$setSpec = $response->createElement(
|
||||||
$set->appendChild($setSpec);
|
localName: 'setSpec',
|
||||||
|
value: $oaiSet->getSpec()
|
||||||
|
);
|
||||||
|
$set->appendChild(node: $setSpec);
|
||||||
|
|
||||||
$setName = $document->createElement('setName', $oaiSet->getName());
|
$setName = $response->createElement(
|
||||||
$set->appendChild($setName);
|
localName: 'setName',
|
||||||
|
value: $oaiSet->getName()
|
||||||
|
);
|
||||||
|
$set->appendChild(node: $setName);
|
||||||
|
|
||||||
if ($oaiSet->hasDescription()) {
|
if ($oaiSet->hasDescription()) {
|
||||||
$setDescription = $document->createElement('setDescription');
|
$setDescription = $response->createElement(localName: 'setDescription');
|
||||||
$set->appendChild($setDescription);
|
$set->appendChild(node: $setDescription);
|
||||||
|
|
||||||
/** @var string */
|
$data = $response->importData(data: $oaiSet->getDescription());
|
||||||
$description = $oaiSet->getDescription();
|
$setDescription->appendChild(node: $data);
|
||||||
$data = $document->importData($description);
|
|
||||||
$setDescription->appendChild($data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($oldToken) || isset($newToken)) {
|
$this->preparedResponse = $response;
|
||||||
$resumptionToken = $document->createElement('resumptionToken');
|
|
||||||
$list->appendChild($resumptionToken);
|
|
||||||
if (isset($newToken)) {
|
|
||||||
$resumptionToken->nodeValue = $newToken->getToken();
|
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'expirationDate',
|
|
||||||
$newToken->getValidUntil()->format('Y-m-d\TH:i:s\Z')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'completeListSize',
|
|
||||||
(string) $completeListSize
|
|
||||||
);
|
|
||||||
$resumptionToken->setAttribute(
|
|
||||||
'cursor',
|
|
||||||
(string) ($counter * $maxRecords)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->preparedResponse = $document;
|
$this->addResumptionToken(
|
||||||
|
node: $list,
|
||||||
|
token: $sets->getResumptionToken() ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Repository;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use OCC\OaiPmh2\Entity\Format;
|
||||||
|
use OCC\OaiPmh2\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Repository for formats.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @extends EntityRepository<Format>
|
||||||
|
*/
|
||||||
|
final class FormatRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add or update metadata format.
|
||||||
|
*
|
||||||
|
* @param Format $entity The metadata format
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addOrUpdate(Format $entity): void
|
||||||
|
{
|
||||||
|
$oldFormat = $this->find(id: $entity->getPrefix());
|
||||||
|
if (isset($oldFormat)) {
|
||||||
|
$oldFormat->setNamespace(namespace: $entity->getNamespace());
|
||||||
|
$oldFormat->setSchema(schema: $entity->getSchema());
|
||||||
|
} else {
|
||||||
|
$this->getEntityManager()->persist(object: $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete metadata format and all associated records.
|
||||||
|
*
|
||||||
|
* @param Format $entity The metadata format
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function delete(Format $entity): void
|
||||||
|
{
|
||||||
|
/** @var EntityManager */
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->remove(object: $entity);
|
||||||
|
$entityManager->flush();
|
||||||
|
$entityManager->pruneOrphanedSets();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Repository;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Entity\Record;
|
||||||
|
use OCC\OaiPmh2\EntityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Repository for records.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @extends EntityRepository<Record>
|
||||||
|
*/
|
||||||
|
final class RecordRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add or update record.
|
||||||
|
*
|
||||||
|
* @param Record $entity The record
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addOrUpdate(Record $entity): void
|
||||||
|
{
|
||||||
|
/** @var EntityManager */
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$oldRecord = $this->find(
|
||||||
|
id: [
|
||||||
|
'identifier' => $entity->getIdentifier(),
|
||||||
|
'format' => $entity->getFormat()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (isset($oldRecord)) {
|
||||||
|
if ($entity->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
|
||||||
|
$oldRecord->setContent(data: $entity->getContent(), validate: false);
|
||||||
|
$oldRecord->setLastChanged(dateTime: $entity->getLastChanged());
|
||||||
|
$newSets = $entity->getSets()->toArray();
|
||||||
|
$oldSets = $oldRecord->getSets()->toArray();
|
||||||
|
// Add new sets.
|
||||||
|
foreach (array_diff(array: $newSets, arrays: $oldSets) as $newSet) {
|
||||||
|
$oldRecord->addSet(set: $newSet);
|
||||||
|
}
|
||||||
|
// Remove old sets.
|
||||||
|
foreach (array_diff(array: $oldSets, arrays: $newSets) as $oldSet) {
|
||||||
|
$oldRecord->removeSet(set: $oldSet);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$entityManager->remove(object: $oldRecord);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($entity->hasContent() || Configuration::getInstance()->deletedRecords !== 'no') {
|
||||||
|
$entityManager->persist(object: $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a record.
|
||||||
|
*
|
||||||
|
* @param Record $entity The record
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function delete(Record $entity): void
|
||||||
|
{
|
||||||
|
/** @var EntityManager */
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
if (Configuration::getInstance()->deletedRecords === 'no') {
|
||||||
|
$entityManager->remove(object: $entity);
|
||||||
|
$entityManager->flush();
|
||||||
|
$entityManager->pruneOrphanedSets();
|
||||||
|
} else {
|
||||||
|
$entity->setContent();
|
||||||
|
$entity->setLastChanged(dateTime: new DateTime());
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Repository;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Entity\Set;
|
||||||
|
use OCC\OaiPmh2\ResultSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Repository for sets.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @extends EntityRepository<Set>
|
||||||
|
*/
|
||||||
|
final class SetRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add or update set.
|
||||||
|
*
|
||||||
|
* @param Set $entity The set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addOrUpdate(Set $entity): void
|
||||||
|
{
|
||||||
|
$oldSet = $this->find(id: $entity->getSpec());
|
||||||
|
if (isset($oldSet)) {
|
||||||
|
$oldSet->setName(name: $entity->getName());
|
||||||
|
$oldSet->setDescription(description: $entity->getDescription());
|
||||||
|
} else {
|
||||||
|
$this->getEntityManager()->persist(object: $entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete set.
|
||||||
|
*
|
||||||
|
* @param Set $entity The set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function delete(Set $entity): void
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->remove(object: $entity);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Repository;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use OCC\OaiPmh2\Entity\Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Repository for resumption tokens.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*
|
||||||
|
* @extends EntityRepository<Token>
|
||||||
|
*/
|
||||||
|
final class TokenRepository extends EntityRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add resumption token.
|
||||||
|
*
|
||||||
|
* @param Token $entity The resumption token
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addOrUpdate(Token $entity): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->persist(object: $entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete resumption token.
|
||||||
|
*
|
||||||
|
* @param Token $entity The resumption token
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function delete(Token $entity): void
|
||||||
|
{
|
||||||
|
$entityManager = $this->getEntityManager();
|
||||||
|
$entityManager->remove(object: $entity);
|
||||||
|
$entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,22 +35,17 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*/
|
*/
|
||||||
class Document
|
final class Response
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* This holds the DOMDocument of the OAI-PMH XML response.
|
* This holds the DOMDocument of the OAI-PMH XML response.
|
||||||
*/
|
*/
|
||||||
protected DOMDocument $dom;
|
private DOMDocument $dom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the root node of the OAI-PMH XML response.
|
* This holds the root node of the OAI-PMH XML response.
|
||||||
*/
|
*/
|
||||||
protected DOMElement $rootNode;
|
private DOMElement $rootNode;
|
||||||
|
|
||||||
/**
|
|
||||||
* This holds the current server request.
|
|
||||||
*/
|
|
||||||
protected ServerRequestInterface $serverRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add XSL processing instructions to XML response document.
|
* Add XSL processing instructions to XML response document.
|
||||||
|
@ -61,24 +56,24 @@ class Document
|
||||||
{
|
{
|
||||||
$uri = $this->serverRequest->getUri();
|
$uri = $this->serverRequest->getUri();
|
||||||
$basePath = $uri->getPath();
|
$basePath = $uri->getPath();
|
||||||
if (str_ends_with($basePath, 'index.php')) {
|
if (str_ends_with(haystack: $basePath, needle: 'index.php')) {
|
||||||
$basePath = pathinfo($basePath, PATHINFO_DIRNAME);
|
$basePath = pathinfo(path: $basePath, flags: PATHINFO_DIRNAME);
|
||||||
}
|
}
|
||||||
$stylesheet = Uri::composeComponents(
|
$stylesheet = Uri::composeComponents(
|
||||||
$uri->getScheme(),
|
scheme: $uri->getScheme(),
|
||||||
$uri->getAuthority(),
|
authority: $uri->getAuthority(),
|
||||||
rtrim($basePath, '/') . '/resources/stylesheet.xsl',
|
path: rtrim(string: $basePath, characters: '/') . '/resources/stylesheet.xsl',
|
||||||
null,
|
query: null,
|
||||||
null
|
fragment: null
|
||||||
);
|
);
|
||||||
$xslt = $this->dom->createProcessingInstruction(
|
$xslt = $this->dom->createProcessingInstruction(
|
||||||
'xml-stylesheet',
|
target: 'xml-stylesheet',
|
||||||
sprintf(
|
data: sprintf(
|
||||||
'type="text/xsl" href="%s"',
|
format: 'type="text/xsl" href="%s"',
|
||||||
$stylesheet
|
values: $stylesheet
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->dom->appendChild($xslt);
|
$this->dom->appendChild(node: $xslt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,20 +85,27 @@ class Document
|
||||||
{
|
{
|
||||||
$uri = $this->serverRequest->getUri();
|
$uri = $this->serverRequest->getUri();
|
||||||
$baseUrl = Uri::composeComponents(
|
$baseUrl = Uri::composeComponents(
|
||||||
$uri->getScheme(),
|
scheme: $uri->getScheme(),
|
||||||
$uri->getAuthority(),
|
authority: $uri->getAuthority(),
|
||||||
$uri->getPath(),
|
path: $uri->getPath(),
|
||||||
null,
|
query: null,
|
||||||
null
|
fragment: null
|
||||||
|
);
|
||||||
|
$request = $this->createElement(
|
||||||
|
localName: 'request',
|
||||||
|
value: $baseUrl,
|
||||||
|
appendToRoot: true
|
||||||
);
|
);
|
||||||
$request = $this->dom->createElement('request', $baseUrl);
|
|
||||||
$this->rootNode->appendChild($request);
|
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
$params = $this->serverRequest->getAttributes();
|
$params = $this->serverRequest->getAttributes();
|
||||||
foreach ($params as $param => $value) {
|
foreach ($params as $param => $value) {
|
||||||
$request->setAttribute(
|
$request->setAttribute(
|
||||||
$param,
|
qualifiedName: $param,
|
||||||
htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8')
|
value: htmlspecialchars(
|
||||||
|
string: $value,
|
||||||
|
flags: ENT_XML1 | ENT_COMPAT,
|
||||||
|
encoding: 'UTF-8'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,8 +117,11 @@ class Document
|
||||||
*/
|
*/
|
||||||
protected function appendResponseDate(): void
|
protected function appendResponseDate(): void
|
||||||
{
|
{
|
||||||
$responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z'));
|
$this->createElement(
|
||||||
$this->rootNode->appendChild($responseDate);
|
localName: 'responseDate',
|
||||||
|
value: gmdate(format: 'Y-m-d\TH:i:s\Z'),
|
||||||
|
appendToRoot: true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,20 +131,20 @@ class Document
|
||||||
*/
|
*/
|
||||||
protected function appendRootElement(): void
|
protected function appendRootElement(): void
|
||||||
{
|
{
|
||||||
$this->rootNode = $this->dom->createElement('OAI-PMH');
|
$this->rootNode = $this->dom->createElement(localName: 'OAI-PMH');
|
||||||
$this->rootNode->setAttribute(
|
$this->rootNode->setAttribute(
|
||||||
'xmlns',
|
qualifiedName: 'xmlns',
|
||||||
'http://www.openarchives.org/OAI/2.0/'
|
value: 'http://www.openarchives.org/OAI/2.0/'
|
||||||
);
|
);
|
||||||
$this->rootNode->setAttribute(
|
$this->rootNode->setAttribute(
|
||||||
'xmlns:xsi',
|
qualifiedName: 'xmlns:xsi',
|
||||||
'http://www.w3.org/2001/XMLSchema-instance'
|
value: 'http://www.w3.org/2001/XMLSchema-instance'
|
||||||
);
|
);
|
||||||
$this->rootNode->setAttribute(
|
$this->rootNode->setAttribute(
|
||||||
'xsi:schemaLocation',
|
qualifiedName: 'xsi:schemaLocation',
|
||||||
'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
|
value: 'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
|
||||||
);
|
);
|
||||||
$this->dom->appendChild($this->rootNode);
|
$this->dom->appendChild(node: $this->rootNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,7 +154,7 @@ class Document
|
||||||
*/
|
*/
|
||||||
protected function createDocument(): void
|
protected function createDocument(): void
|
||||||
{
|
{
|
||||||
$this->dom = new DOMDocument('1.0', 'UTF-8');
|
$this->dom = new DOMDocument(version: '1.0', encoding: 'UTF-8');
|
||||||
$this->dom->preserveWhiteSpace = false;
|
$this->dom->preserveWhiteSpace = false;
|
||||||
$this->addProcessingInstructions();
|
$this->addProcessingInstructions();
|
||||||
}
|
}
|
||||||
|
@ -166,11 +171,15 @@ class Document
|
||||||
public function createElement(string $localName, string $value = '', bool $appendToRoot = false): DOMElement
|
public function createElement(string $localName, string $value = '', bool $appendToRoot = false): DOMElement
|
||||||
{
|
{
|
||||||
$node = $this->dom->createElement(
|
$node = $this->dom->createElement(
|
||||||
$localName,
|
localName: $localName,
|
||||||
htmlspecialchars($value, ENT_XML1, 'UTF-8')
|
value: htmlspecialchars(
|
||||||
|
string: $value,
|
||||||
|
flags: ENT_XML1,
|
||||||
|
encoding: 'UTF-8'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if ($appendToRoot) {
|
if ($appendToRoot) {
|
||||||
$this->rootNode->appendChild($node);
|
$this->rootNode->appendChild(node: $node);
|
||||||
}
|
}
|
||||||
return $node;
|
return $node;
|
||||||
}
|
}
|
||||||
|
@ -182,21 +191,21 @@ class Document
|
||||||
*
|
*
|
||||||
* @return DOMNode The imported XML node
|
* @return DOMNode The imported XML node
|
||||||
*
|
*
|
||||||
* @throws DOMException
|
* @throws DOMException if the data cannot be imported
|
||||||
*/
|
*/
|
||||||
public function importData(string $data): DOMNode
|
public function importData(string $data): DOMNode
|
||||||
{
|
{
|
||||||
$document = new DOMDocument('1.0', 'UTF-8');
|
$document = new DOMDocument(version: '1.0', encoding: 'UTF-8');
|
||||||
$document->preserveWhiteSpace = false;
|
$document->preserveWhiteSpace = false;
|
||||||
if ($document->loadXML($data) === true) {
|
if ($document->loadXML(source: $data) === true) {
|
||||||
/** @var DOMElement */
|
/** @var DOMElement */
|
||||||
$rootNode = $document->documentElement;
|
$rootNode = $document->documentElement;
|
||||||
$node = $this->dom->importNode($rootNode, true);
|
$node = $this->dom->importNode(node: $rootNode, deep: true);
|
||||||
return $node;
|
return $node;
|
||||||
} else {
|
} else {
|
||||||
throw new DOMException(
|
throw new DOMException(
|
||||||
'Could not import the XML data. Most likely it is not well-formed.',
|
message: 'Could not import the XML data. Most likely it is not well-formed.',
|
||||||
500
|
code: 500
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -206,9 +215,8 @@ class Document
|
||||||
*
|
*
|
||||||
* @param ServerRequestInterface $serverRequest The PSR-7 HTTP Server Request
|
* @param ServerRequestInterface $serverRequest The PSR-7 HTTP Server Request
|
||||||
*/
|
*/
|
||||||
public function __construct(ServerRequestInterface $serverRequest)
|
public function __construct(private ServerRequestInterface $serverRequest)
|
||||||
{
|
{
|
||||||
$this->serverRequest = $serverRequest;
|
|
||||||
$this->createDocument();
|
$this->createDocument();
|
||||||
$this->appendRootElement();
|
$this->appendRootElement();
|
||||||
$this->appendResponseDate();
|
$this->appendResponseDate();
|
|
@ -22,13 +22,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCC\OaiPmh2;
|
namespace OCC\OaiPmh2;
|
||||||
|
|
||||||
use Countable;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Iterator;
|
|
||||||
use OCC\Basics\InterfaceTraits\Countable as CountableTrait;
|
|
||||||
use OCC\Basics\InterfaceTraits\Iterator as IteratorTrait;
|
|
||||||
use OCC\OaiPmh2\Entity\Format;
|
|
||||||
use OCC\OaiPmh2\Entity\Record;
|
|
||||||
use OCC\OaiPmh2\Entity\Set;
|
|
||||||
use OCC\OaiPmh2\Entity\Token;
|
use OCC\OaiPmh2\Entity\Token;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,35 +31,15 @@ use OCC\OaiPmh2\Entity\Token;
|
||||||
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
* @package OAIPMH2
|
* @package OAIPMH2
|
||||||
*
|
*
|
||||||
* @template QueryResult of array<string, Format|Record|Set>
|
* @template TEntity of Entity
|
||||||
* @implements Iterator<QueryResult>
|
* @extends ArrayCollection<string, TEntity>
|
||||||
*/
|
*/
|
||||||
class Result implements Countable, Iterator
|
final class ResultSet extends ArrayCollection
|
||||||
{
|
{
|
||||||
use CountableTrait;
|
|
||||||
use IteratorTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This holds the Doctrine result set.
|
|
||||||
*
|
|
||||||
* @var QueryResult
|
|
||||||
*/
|
|
||||||
private array $data;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This holds the optional resumption token.
|
* This holds the optional resumption token.
|
||||||
*/
|
*/
|
||||||
protected ?Token $resumptionToken = null;
|
private ?Token $resumptionToken;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the query result.
|
|
||||||
*
|
|
||||||
* @return QueryResult The result set
|
|
||||||
*/
|
|
||||||
public function getQueryResult(): array
|
|
||||||
{
|
|
||||||
return $this->data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the resumption token.
|
* Get the resumption token.
|
||||||
|
@ -92,10 +66,12 @@ class Result implements Countable, Iterator
|
||||||
/**
|
/**
|
||||||
* Create new result set.
|
* Create new result set.
|
||||||
*
|
*
|
||||||
* @param QueryResult $queryResult The Doctrine result set
|
* @param array<string, TEntity> $elements Array of entities
|
||||||
|
* @param Token $token Optional resumption token
|
||||||
*/
|
*/
|
||||||
public function __construct(array $queryResult)
|
public function __construct(array $elements = [], Token $token = null)
|
||||||
{
|
{
|
||||||
$this->data = $queryResult;
|
parent::__construct(elements: $elements);
|
||||||
|
$this->resumptionToken = $token;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for configuration settings.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*/
|
||||||
|
class ConfigurationValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get constraints for configuration array.
|
||||||
|
*
|
||||||
|
* @return array<Constraint> The collection of constraints
|
||||||
|
*/
|
||||||
|
protected static function getValidationConstraints(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Assert\Collection(
|
||||||
|
fields: [
|
||||||
|
'repositoryName' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
],
|
||||||
|
'adminEmail' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\Email(options: ['mode' => 'html5']),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
],
|
||||||
|
'database' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
],
|
||||||
|
'metadataPrefix' => [
|
||||||
|
new Assert\Type(type: 'array'),
|
||||||
|
new Assert\All(
|
||||||
|
constraints: [
|
||||||
|
new Assert\Collection(
|
||||||
|
fields: [
|
||||||
|
'schema' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\Url(),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
],
|
||||||
|
'namespace' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\Url(),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
'deletedRecords' => [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\Choice(options: ['no', 'persistent', 'transient']),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
],
|
||||||
|
'maxRecords' => [
|
||||||
|
new Assert\Type(type: 'int'),
|
||||||
|
new Assert\Range(options: ['min' => 1, 'max' => 100])
|
||||||
|
],
|
||||||
|
'tokenValid' => [
|
||||||
|
new Assert\Type(type: 'int'),
|
||||||
|
new Assert\Range(options: ['min' => 300, 'max' => 86400])
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given configuration array.
|
||||||
|
*
|
||||||
|
* @param array<array-key, mixed> $config The configuration array to validate
|
||||||
|
*
|
||||||
|
* @return ConstraintViolationListInterface The list of violations
|
||||||
|
*/
|
||||||
|
public static function validate(array $config): ConstraintViolationListInterface
|
||||||
|
{
|
||||||
|
return Validation::createValidator()->validate(
|
||||||
|
value: $config,
|
||||||
|
constraints: self::getValidationConstraints()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for Regular Expressions.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*/
|
||||||
|
class RegExValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get constraints for regular expression.
|
||||||
|
*
|
||||||
|
* @param string $regEx The regular expression for validation
|
||||||
|
*
|
||||||
|
* @return array<Constraint> The array of constraints
|
||||||
|
*/
|
||||||
|
protected static function getValidationConstraints(string $regEx): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Assert\Regex(
|
||||||
|
pattern: [
|
||||||
|
'pattern' => $regEx,
|
||||||
|
'message' => 'This value does not match the regular expression "{{ pattern }}".'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string matches a given regular expression.
|
||||||
|
*
|
||||||
|
* @param string $string The string
|
||||||
|
* @param string $regEx The regular expression
|
||||||
|
*
|
||||||
|
* @return ConstraintViolationListInterface The list of violations
|
||||||
|
*/
|
||||||
|
public static function validate(string $string, string $regEx): ConstraintViolationListInterface
|
||||||
|
{
|
||||||
|
return Validation::createValidator()->validate(
|
||||||
|
value: $string,
|
||||||
|
constraints: self::getValidationConstraints(regEx: $regEx)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for URLs.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*/
|
||||||
|
class UrlValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get constraints for URLs.
|
||||||
|
*
|
||||||
|
* @return array<Constraint> The array of constraints
|
||||||
|
*/
|
||||||
|
protected static function getValidationConstraints(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Assert\Url(),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given string is a valid URL.
|
||||||
|
*
|
||||||
|
* @param string $url The URL
|
||||||
|
*
|
||||||
|
* @return ConstraintViolationListInterface The list of violations
|
||||||
|
*/
|
||||||
|
public static function validate(string $url): ConstraintViolationListInterface
|
||||||
|
{
|
||||||
|
return Validation::createValidator()->validate(
|
||||||
|
value: $url,
|
||||||
|
constraints: self::getValidationConstraints()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2024 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCC\OaiPmh2\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\ConstraintViolation;
|
||||||
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for XML.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package OAIPMH2
|
||||||
|
*/
|
||||||
|
class XmlValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get constraints for XML.
|
||||||
|
*
|
||||||
|
* @return array<Constraint> The array of constraints
|
||||||
|
*/
|
||||||
|
protected static function getValidationConstraints(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Assert\Type(type: 'string'),
|
||||||
|
new Assert\NotBlank()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given string is valid XML.
|
||||||
|
*
|
||||||
|
* @param string $xml The XML string
|
||||||
|
*
|
||||||
|
* @return ConstraintViolationListInterface The list of violations
|
||||||
|
*/
|
||||||
|
public static function validate(string $xml): ConstraintViolationListInterface
|
||||||
|
{
|
||||||
|
$violations = Validation::createValidator()->validate(
|
||||||
|
value: $xml,
|
||||||
|
constraints: self::getValidationConstraints()
|
||||||
|
);
|
||||||
|
if (simplexml_load_string(data: $xml) === false) {
|
||||||
|
$violations->add(
|
||||||
|
violation: new ConstraintViolation(
|
||||||
|
message: 'Value could not be parsed as XML.',
|
||||||
|
messageTemplate: 'Value could not be parsed as XML.',
|
||||||
|
parameters: [],
|
||||||
|
root: $xml,
|
||||||
|
propertyPath: null,
|
||||||
|
invalidValue: $xml
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $violations;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue