Initial upload (still WIP)
This commit is contained in:
parent
121cdf8010
commit
04583643e0
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -0,0 +1,4 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: sebastian-meyer
|
||||||
|
custom: "https://paypal.me/sebastianmeyer"
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "composer"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
assignees:
|
||||||
|
- "sebastian-meyer"
|
||||||
|
labels: [ ]
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
assignees:
|
||||||
|
- "sebastian-meyer"
|
||||||
|
labels: [ ]
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: PHP Mess Detector
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
PHPMD:
|
||||||
|
name: PHPMD Scanner
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Environment
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: "8.0"
|
||||||
|
coverage: none
|
||||||
|
tools: phpmd
|
||||||
|
|
||||||
|
- name: Run PHPMD
|
||||||
|
run: phpmd . sarif codesize --reportfile phpmd-results.sarif
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Upload Analysis Results
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
with:
|
||||||
|
sarif_file: phpmd-results.sarif
|
||||||
|
wait-for-processing: true
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: PHP Static Analyzer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
PHPStan:
|
||||||
|
name: PHPStan Scanner
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Environment
|
||||||
|
uses: php-actions/composer@v6
|
||||||
|
with:
|
||||||
|
command: update
|
||||||
|
php_version: "8.0"
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
uses: php-actions/phpstan@v3
|
||||||
|
with:
|
||||||
|
path: src/
|
||||||
|
configuration: phpstan.dist.neon
|
|
@ -1,6 +1,7 @@
|
||||||
composer.phar
|
/.vscode/
|
||||||
|
/config/config.yml
|
||||||
|
/data/
|
||||||
|
/var/
|
||||||
/vendor/
|
/vendor/
|
||||||
|
.php-cs-fixer.php
|
||||||
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
|
phpstan.neon
|
||||||
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
|
|
||||||
# composer.lock
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful PHP Basics
|
||||||
|
* Copyright (C) 2023 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 PhpCsFixer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for PHP-CS-Fixer.
|
||||||
|
* @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/config.rst
|
||||||
|
*
|
||||||
|
* @return ConfigInterface
|
||||||
|
*/
|
||||||
|
return (new Config())
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@PSR12' => true,
|
||||||
|
])
|
||||||
|
->setFinder(
|
||||||
|
(new Finder())->in(__DIR__)
|
||||||
|
);
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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 Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||||
|
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||||
|
use Exception;
|
||||||
|
use OCC\OaiPmh2\Console\AddRecordCommand;
|
||||||
|
use OCC\OaiPmh2\Console\BulkUpdateCommand;
|
||||||
|
use OCC\OaiPmh2\Console\DeleteRecordCommand;
|
||||||
|
use OCC\OaiPmh2\Console\PruneResumptionTokensCommand;
|
||||||
|
use OCC\OaiPmh2\Console\UpdateFormatsCommand;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$commands = [
|
||||||
|
new AddRecordCommand(),
|
||||||
|
new BulkUpdateCommand(),
|
||||||
|
new DeleteRecordCommand(),
|
||||||
|
new PruneResumptionTokensCommand(),
|
||||||
|
new UpdateFormatsCommand()
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
ConsoleRunner::run(
|
||||||
|
new SingleManagerProvider(
|
||||||
|
Database::getInstance()->getEntityManager()
|
||||||
|
),
|
||||||
|
$commands
|
||||||
|
);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
echo '[ERROR] Exception ' . $exception->getCode() . ' thrown:' . PHP_EOL;
|
||||||
|
echo $exception->getMessage() . PHP_EOL;
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"name": "opencultureconsulting/oai-pmh2",
|
||||||
|
"description": "This is a stand-alone OAI-PMH 2.0 data provider. It serves records in any XML metadata format from a database, supports deleted records, resumption tokens and sets.",
|
||||||
|
"type": "project",
|
||||||
|
"keywords": [
|
||||||
|
"oai",
|
||||||
|
"oaipmh",
|
||||||
|
"oaipmh2",
|
||||||
|
"oai-pmh",
|
||||||
|
"oai-pmh2",
|
||||||
|
"code4lib"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/opencultureconsulting/oai-pmh2",
|
||||||
|
"readme": "README.md",
|
||||||
|
"license": ["GPL-3.0-or-later"],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sebastian Meyer",
|
||||||
|
"email": "sebastian.meyer@opencultureconsulting.com",
|
||||||
|
"homepage": "https://www.opencultureconsulting.com",
|
||||||
|
"role": "maintainer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/opencultureconsulting/oai-pmh2/issues",
|
||||||
|
"source": "https://github.com/opencultureconsulting/oai-pmh2",
|
||||||
|
"docs": "https://github.com/opencultureconsulting/oai-pmh2/blob/main/README.md"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"ext-sqlite3": "*",
|
||||||
|
"doctrine/dbal": "^3.7",
|
||||||
|
"doctrine/orm": "^2.17",
|
||||||
|
"opencultureconsulting/basics": "^1.0",
|
||||||
|
"opencultureconsulting/psr15": "^1.0",
|
||||||
|
"symfony/cache": "^6.4",
|
||||||
|
"symfony/console": "^6.4",
|
||||||
|
"symfony/filesystem": "^6.4",
|
||||||
|
"symfony/serializer":"^6.4",
|
||||||
|
"symfony/validator": "^6.4",
|
||||||
|
"symfony/yaml": "^6.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.5",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.45"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"OCC\\OaiPmh2\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"pre-install-cmd": [
|
||||||
|
"@php -r \"!is_dir('./data') && mkdir('./data', 0775);\"",
|
||||||
|
"@php -r \"!file_exists('./config/config.yml') && copy('./config/config.dist.yml', './config/config.yml');\""
|
||||||
|
],
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@doctrine:clear-cache --quiet",
|
||||||
|
"@php bin/cli orm:generate-proxies --quiet"
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@doctrine:initialize-database --quiet"
|
||||||
|
],
|
||||||
|
"doctrine:clear-cache": [
|
||||||
|
"@php bin/cli orm:clear-cache:metadata --flush",
|
||||||
|
"@php bin/cli orm:clear-cache:query --flush",
|
||||||
|
"@php bin/cli orm:clear-cache:result --flush"
|
||||||
|
],
|
||||||
|
"doctrine:initialize-database": [
|
||||||
|
"@php bin/cli orm:schema-tool:update --complete --force",
|
||||||
|
"@oai:update-formats --quiet"
|
||||||
|
],
|
||||||
|
"oai:update-formats": [
|
||||||
|
"@php bin/cli oai:formats:update"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts-descriptions": {
|
||||||
|
"doctrine:clear-cache": "Clears the Doctrine/ORM metadata, query and result caches",
|
||||||
|
"doctrine:generate-proxies": "Generates the Doctrine/ORM entity proxies",
|
||||||
|
"doctrine:initialize-database": "Initializes a new database (NOT RECOMMENDED IN PRODUCTION!)",
|
||||||
|
"oai:update-formats": "Updates supported metadata formats from configuration"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,90 @@
|
||||||
|
#
|
||||||
|
# The default configuration
|
||||||
|
#
|
||||||
|
# Copy this file as 'config.yml' and change the settings to your preferences.
|
||||||
|
# See https://www.openarchives.org/OAI/2.0/openarchivesprotocol.htm for further
|
||||||
|
# explanation.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# A human readable name for the repository
|
||||||
|
#
|
||||||
|
repositoryName: 'OAI-PMH 2.0 Data Provider'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Email address for contacting the repository owner
|
||||||
|
#
|
||||||
|
# This has to be a valid email according to RFC 822 Address Specification.
|
||||||
|
# See https://www.w3.org/Protocols/rfc822/#z8 for further explanation.
|
||||||
|
#
|
||||||
|
adminEmail: admin@example.org
|
||||||
|
|
||||||
|
#
|
||||||
|
# Database connection details
|
||||||
|
#
|
||||||
|
# This has to be a valid data source name (DSN) URL. The scheme is used to
|
||||||
|
# specify a driver, the user and password in the URL encode user and password
|
||||||
|
# for the connection, followed by the host and port parts. The path after the
|
||||||
|
# authority part represents the name of the database (the leading slash is
|
||||||
|
# removed so add an extra slash to specify an absolute file path for SQLite).
|
||||||
|
# The placeholder "%BASEDIR%" may be used to represent the application's base
|
||||||
|
# directory.
|
||||||
|
# Any optional query parameters are used as additional connection parameters.
|
||||||
|
# Since the scheme determines the database driver, it also specifies if the PDO
|
||||||
|
# abstraction ("mariadb", "mssql", "mysql", "oracle", "postgres", "sqlite") or
|
||||||
|
# native drivers ("ibm-db2", "mysqli", "oci8", "pgsql", "sqlite3", "sqlsrv")
|
||||||
|
# should be used to handle the connection. Make sure the corresponding PHP
|
||||||
|
# extensions are installed.
|
||||||
|
# See https://www.doctrine-project.org/projects/doctrine-dbal/en/3.7/reference/configuration.html#connecting-using-a-url
|
||||||
|
# for further explanation.
|
||||||
|
#
|
||||||
|
# %DRIVER%://[%USER%[:%PASSWORD%]@]%HOST%[:%PORT%]/%DBNAME%[?%OPTIONS%]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# database: 'mssql://oaipmh:secret@127.0.0.1/oaipmh'
|
||||||
|
# database: 'mysql://root@localhost/oai?charset=utf8mb4'
|
||||||
|
# database: 'pgsql://oaipmh:secret@localhost:5432/oai_data_provider'
|
||||||
|
# database: 'sqlite3:////home/oaipmh/database.db'
|
||||||
|
#
|
||||||
|
# Run "composer doctrine:initialize-database" after switching to a new DB to
|
||||||
|
# test the settings and initialize the database!
|
||||||
|
#
|
||||||
|
database: 'sqlite3:///%BASEDIR%/data/sqlite3.db'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Metadata formats, namespaces and schemas of your records
|
||||||
|
#
|
||||||
|
# The default is 'oai_dc' which is also required by the OAI-PMH specification,
|
||||||
|
# but technically you can provide any XML based data formats you want. Just add
|
||||||
|
# another entry with the metadata prefix as key and namespace/schema URIs as
|
||||||
|
# array values or replace the default entry (although not recommended).
|
||||||
|
# You do not have to provide every record in each metadata format, but if you
|
||||||
|
# have the same record in multiple formats, it's highly recommended to use the
|
||||||
|
# same identifier for all versions of the record.
|
||||||
|
#
|
||||||
|
# Run "composer oai:update-formats" after changing metadata prefixes to update
|
||||||
|
# the database accordingly!
|
||||||
|
#
|
||||||
|
metadataPrefix: {
|
||||||
|
oai_dc: {
|
||||||
|
namespace: 'http://www.openarchives.org/OAI/2.0/oai_dc/',
|
||||||
|
schema: 'https://www.openarchives.org/OAI/2.0/oai_dc.xsd'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Maximum number of records to return per request
|
||||||
|
#
|
||||||
|
# For larger result sets resumption tokens are provided repeatedly, which
|
||||||
|
# allow requesting more batches of records until the set is complete.
|
||||||
|
#
|
||||||
|
# [1 - 100]
|
||||||
|
#
|
||||||
|
maxRecords: 50
|
||||||
|
|
||||||
|
#
|
||||||
|
# Number of seconds a resumption token should be valid
|
||||||
|
#
|
||||||
|
# [300 - 86400]
|
||||||
|
#
|
||||||
|
tokenValid: 1800 # 30 minutes
|
|
@ -0,0 +1,14 @@
|
||||||
|
#
|
||||||
|
# Configuration for PHPStan.
|
||||||
|
# @see https://phpstan.org/config-reference
|
||||||
|
#
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 9
|
||||||
|
strictRules:
|
||||||
|
noVariableVariables: false
|
||||||
|
paths:
|
||||||
|
- src
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$app = new App();
|
||||||
|
$app->run();
|
|
@ -0,0 +1,520 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2006 Christopher Gutteridge <cjg@ecs.soton.ac.uk>
|
||||||
|
* Copyright (C) 2023 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/>.
|
||||||
|
-->
|
||||||
|
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:oai="http://www.openarchives.org/OAI/2.0/">
|
||||||
|
|
||||||
|
<xsl:output method="html" />
|
||||||
|
|
||||||
|
<xsl:template name="style">
|
||||||
|
* {
|
||||||
|
font-family: "Lucida Sans Unicode", sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 1em 2em 1em 2em;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
td.key {
|
||||||
|
background-color: #e0e0ff;
|
||||||
|
padding: 3px;
|
||||||
|
text-align: right;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
td.value {
|
||||||
|
vertical-align: center;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
border: 1px outset #88f;
|
||||||
|
background-color: #c0c0ff;
|
||||||
|
padding: 1px 4px;
|
||||||
|
font-size: 80%;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
.link:active {
|
||||||
|
color: red;
|
||||||
|
border: 1px inset #88f;
|
||||||
|
background-color: #a0a0df;
|
||||||
|
}
|
||||||
|
.results {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
div.quicklinks {
|
||||||
|
border-bottom: 2px solid #ccc;
|
||||||
|
border-top: 2px solid #ccc;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 4px;
|
||||||
|
text-align: left;
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
ul li {
|
||||||
|
font-size: 80%;
|
||||||
|
display: inline;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ol>li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 5px 5px;
|
||||||
|
margin: 0 0 1em;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
p.info {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
.xmlSource {
|
||||||
|
font-size: 70%;
|
||||||
|
border: solid #c0c0a0 1px;
|
||||||
|
background-color: #ffffe0;
|
||||||
|
padding: 2em 2em 2em 0;
|
||||||
|
}
|
||||||
|
.xmlBlock {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
.xmlTagName {
|
||||||
|
color: #800000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.xmlAttrName {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.xmlAttrValue {
|
||||||
|
color: #0000c0;
|
||||||
|
}
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:variable name='verb' select="/oai:OAI-PMH/oai:request/@verb"/>
|
||||||
|
<xsl:variable name='metadataPrefix'>
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="/oai:OAI-PMH/oai:request/@metadataPrefix != ''">
|
||||||
|
<xsl:value-of select="/oai:OAI-PMH/oai:request/@metadataPrefix"/>
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:when test="/oai:OAI-PMH/oai:request/@resumptionToken != ''">
|
||||||
|
<xsl:value-of select="substring-after(/oai:OAI-PMH/oai:request/@resumptionToken,'_')"/>
|
||||||
|
</xsl:when>
|
||||||
|
</xsl:choose>
|
||||||
|
</xsl:variable>
|
||||||
|
<xsl:variable name='identifier' select="/oai:OAI-PMH/oai:request/@identifier"/>
|
||||||
|
<xsl:variable name='from' select="/oai:OAI-PMH/oai:request/@from"/>
|
||||||
|
<xsl:variable name='until' select="/oai:OAI-PMH/oai:request/@until"/>
|
||||||
|
<xsl:variable name='set' select="/oai:OAI-PMH/oai:request/@set"/>
|
||||||
|
<xsl:variable name='resumptionToken' select="/oai:OAI-PMH/oai:request/@resumptionToken"/>
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>OAI-PMH 2.0 Request Results</title>
|
||||||
|
<style><xsl:call-template name="style"/></style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>OAI-PMH 2.0 Request Results</h1>
|
||||||
|
<xsl:call-template name="quicklinks"/>
|
||||||
|
<xsl:apply-templates select="/oai:OAI-PMH"/>
|
||||||
|
<xsl:call-template name="quicklinks"/>
|
||||||
|
<p class="info">You are viewing an HTML version of the XML OAI-PMH response. To see the underlying XML as it appears to any OAI-PMH harvester use your web browser's <em>view source</em> option or disable XSLT processing.</p>
|
||||||
|
<p class="info">This XSL script was originally written by Christopher Gutteridge at <a href="https://www.southampton.ac.uk/">University of Southampton</a> for the <a href="https://www.eprints.org/">EPrints</a> project and was later adapted by Sebastian Meyer at <a href="https://www.opencultureconsulting.com/">Open Culture Consulting</a> to be more generally applicable to other OAI-PMH interfaces. It is available on <a href="https://github.com/opencultureconsulting/oai-pmh2">GitHub</a> for free!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template name="quicklinks">
|
||||||
|
<div class="quicklinks">
|
||||||
|
<ul>
|
||||||
|
<li>» <a class="link" href="?verb=Identify">Identify</a></li>
|
||||||
|
<li>» <a class="link" href="?verb=ListMetadataFormats">ListMetadataFormats</a></li>
|
||||||
|
<xsl:if test="$identifier">
|
||||||
|
<li>» <a class="link" href="?verb=ListMetadataFormats&identifier={$identifier}">ListMetadataFormats (for record <em><xsl:value-of select="$identifier"/></em>)</a></li>
|
||||||
|
</xsl:if>
|
||||||
|
<xsl:if test="$metadataPrefix != ''">
|
||||||
|
<li>» <a class="link" href="?verb=ListIdentifiers&metadataPrefix={$metadataPrefix}">ListIdentifiers (for format <em><xsl:value-of select="$metadataPrefix"/></em>)</a></li>
|
||||||
|
<li>» <a class="link" href="?verb=ListRecords&metadataPrefix={$metadataPrefix}">ListRecords (for format <em><xsl:value-of select="$metadataPrefix"/></em>)</a></li>
|
||||||
|
<xsl:if test="$identifier">
|
||||||
|
<li>» <a class="link" href="?verb=GetRecord&metadataPrefix={$metadataPrefix}&identifier={$identifier}">GetRecord (<em><xsl:value-of select="$identifier"/></em> in <em><xsl:value-of select="$metadataPrefix"/></em>)</a></li>
|
||||||
|
</xsl:if>
|
||||||
|
</xsl:if>
|
||||||
|
<li>» <a class="link" href="?verb=ListSets">ListSets</a></li>
|
||||||
|
<xsl:if test="//oai:resumptionToken">
|
||||||
|
<li>» <a class="link" href="?verb={$verb}&resumptionToken={//oai:resumptionToken}">Resume</a></li>
|
||||||
|
</xsl:if>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="/oai:OAI-PMH">
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Datestamp of Response</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:responseDate"/></td></tr>
|
||||||
|
<tr><td class="key">Request URL</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:request"/></td></tr>
|
||||||
|
<tr><td class="key">Request Parameters</td>
|
||||||
|
<td class="value">
|
||||||
|
<xsl:if test="oai:request/@verb">verb = <em><xsl:value-of select="$verb"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@metadataPrefix">metadataPrefix = <em><xsl:value-of select="$metadataPrefix"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@identifier">identifier = <em><xsl:value-of select="$identifier"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@from">from = <em><xsl:value-of select="$from"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@until">until = <em><xsl:value-of select="$until"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@set">set = <em><xsl:value-of select="$set"/></em><br/></xsl:if>
|
||||||
|
<xsl:if test="oai:request/@resumptionToken">resumptionToken = <em><xsl:value-of select="$resumptionToken"/></em><br/></xsl:if>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="oai:error">
|
||||||
|
<h2>Error</h2>
|
||||||
|
<p>The request could not be completed due to the following error.</p>
|
||||||
|
<div class="results">
|
||||||
|
<xsl:apply-templates select="oai:error"/>
|
||||||
|
</div>
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:otherwise>
|
||||||
|
<h2><xsl:value-of select="$verb"/></h2>
|
||||||
|
<p>The request was completed with the following results.</p>
|
||||||
|
<div class="results">
|
||||||
|
<xsl:apply-templates select="oai:Identify" />
|
||||||
|
<xsl:apply-templates select="oai:ListMetadataFormats"/>
|
||||||
|
<xsl:apply-templates select="oai:ListIdentifiers"/>
|
||||||
|
<xsl:apply-templates select="oai:ListRecords"/>
|
||||||
|
<xsl:apply-templates select="oai:GetRecord"/>
|
||||||
|
</div>
|
||||||
|
</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Error
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:error">
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Error Code</td>
|
||||||
|
<td class="value"><xsl:value-of select="@code"/></td></tr>
|
||||||
|
</table>
|
||||||
|
<p class="error"><xsl:value-of select="."/></p>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Identify
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:Identify">
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<h3>Repository Identification</h3>
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Name</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:repositoryName"/></td></tr>
|
||||||
|
<tr><td class="key">Base URL</td>
|
||||||
|
<td class="value"><a href="{oai:baseURL}"><xsl:value-of select="oai:baseURL"/></a></td></tr>
|
||||||
|
<tr><td class="key">Protocol Version</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:protocolVersion"/></td></tr>
|
||||||
|
<tr><td class="key">Earliest Datestamp</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:earliestDatestamp"/></td></tr>
|
||||||
|
<tr><td class="key">Deleted Record Policy</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:deletedRecord"/></td></tr>
|
||||||
|
<tr><td class="key">Granularity</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:granularity"/></td></tr>
|
||||||
|
<tr><td class="key">Administrative Email</td>
|
||||||
|
<td class="value"><a href="mailto:{oai:adminEmail}"><xsl:value-of select="oai:adminEmail"/></a></td></tr>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ListMetadataFormats
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:ListMetadataFormats">
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="$identifier">
|
||||||
|
<p class="info">This is a list of metadata formats available for the record <em><xsl:value-of select="$identifier"/></em>.</p>
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:otherwise>
|
||||||
|
<p class="info">This is a list of metadata formats available from this repository.</p>
|
||||||
|
</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
<ol>
|
||||||
|
<xsl:apply-templates select="oai:metadataFormat"/>
|
||||||
|
</ol>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Metadata Format Details
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:metadataFormat">
|
||||||
|
<li>
|
||||||
|
<h3>Metadata Format <em><xsl:value-of select="oai:metadataPrefix"/></em></h3>
|
||||||
|
<ul>
|
||||||
|
<li>» <a class="link" href="?verb=ListIdentifiers&metadataPrefix={oai:metadataPrefix}">ListIdentifiers</a></li>
|
||||||
|
<li>» <a class="link" href="?verb=ListRecords&metadataPrefix={oai:metadataPrefix}">ListRecords</a></li>
|
||||||
|
<xsl:if test="$identifier"><li>» <a class="link" href="?verb=GetRecord&metadataPrefix={oai:metadataPrefix}&identifier={$identifier}">GetRecord</a></li></xsl:if>
|
||||||
|
</ul>
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Prefix</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:metadataPrefix"/></td></tr>
|
||||||
|
<tr><td class="key">Namespace</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:metadataNamespace"/></td></tr>
|
||||||
|
<tr><td class="key">Schema</td>
|
||||||
|
<td class="value"><a href="{oai:schema}"><xsl:value-of select="oai:schema"/></a></td></tr>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ListIdentifiers
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:ListIdentifiers">
|
||||||
|
<p class="info">This is a list of records' identifiers available for the metadata format <em><xsl:value-of select="$metadataPrefix"/></em>.</p>
|
||||||
|
<ol>
|
||||||
|
<xsl:apply-templates select="oai:header" />
|
||||||
|
</ol>
|
||||||
|
<xsl:apply-templates select="oai:resumptionToken" />
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="oai:ListIdentifiers/oai:header">
|
||||||
|
<li>
|
||||||
|
<h3>Record Header <em><xsl:value-of select="oai:identifier"/></em></h3>
|
||||||
|
<ul>
|
||||||
|
<li>» <a class="link" href="?verb=ListMetadataFormats&identifier={oai:identifier}">ListMetadataFormats</a></li>
|
||||||
|
<li>» <a class="link" href="?verb=GetRecord&metadataPrefix={$metadataPrefix}&identifier={oai:identifier}">GetRecord</a></li>
|
||||||
|
</ul>
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Identifier</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:identifier"/></td></tr>
|
||||||
|
<tr><td class="key">Datestamp</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:datestamp"/></td></tr>
|
||||||
|
<tr><td class="key">Deleted</td>
|
||||||
|
<td class="value">
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="@status = 'deleted'">yes</xsl:when>
|
||||||
|
<xsl:otherwise>no</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ListRecords
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:ListRecords">
|
||||||
|
<p class="info">This is a list of records available for the metadata format <em><xsl:value-of select="$metadataPrefix"/></em>.</p>
|
||||||
|
<ol>
|
||||||
|
<xsl:apply-templates select="oai:record" />
|
||||||
|
</ol>
|
||||||
|
<xsl:apply-templates select="oai:resumptionToken" />
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
GetRecord
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:GetRecord">
|
||||||
|
<p class="info">This is the record <em><xsl:value-of select="$identifier"/></em> in the metadata format <em><xsl:value-of select="$metadataPrefix"/></em>.</p>
|
||||||
|
<ol>
|
||||||
|
<xsl:apply-templates select="oai:record" />
|
||||||
|
</ol>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Record Details
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:record">
|
||||||
|
<li>
|
||||||
|
<xsl:apply-templates select="oai:header" />
|
||||||
|
<xsl:apply-templates select="oai:metadata" />
|
||||||
|
</li>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="oai:record/oai:header">
|
||||||
|
<h3>Record <em><xsl:value-of select="oai:identifier"/></em></h3>
|
||||||
|
<ul>
|
||||||
|
<li>» <a class="link" href="?verb=ListMetadataFormats&identifier={oai:identifier}">ListMetadataFormats</a></li>
|
||||||
|
<xsl:if test="$verb != 'GetRecord'"><li>» <a class="link" href="?verb=GetRecord&metadataPrefix={$metadataPrefix}&identifier={oai:identifier}">GetRecord</a></li></xsl:if>
|
||||||
|
</ul>
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Identifier</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:identifier"/></td></tr>
|
||||||
|
<tr><td class="key">Datestamp</td>
|
||||||
|
<td class="value"><xsl:value-of select="oai:datestamp"/></td></tr>
|
||||||
|
</table>
|
||||||
|
<xsl:if test="@status = 'deleted'"><h4>This record has been deleted.</h4></xsl:if>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="oai:metadata">
|
||||||
|
<xsl:apply-templates select="*" />
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Resumption Token
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:resumptionToken">
|
||||||
|
<p>There are more results.</p>
|
||||||
|
<ul>
|
||||||
|
<li>» <a class="link" href="?verb={$verb}&resumptionToken={.}">Resume</a></li>
|
||||||
|
</ul>
|
||||||
|
<table class="values">
|
||||||
|
<tr><td class="key">Cursor Position</td>
|
||||||
|
<td class="value"><xsl:value-of select="@cursor"/></td></tr>
|
||||||
|
<tr><td class="key">Total Records</td>
|
||||||
|
<td class="value"><xsl:value-of select="@completeListSize"/></td></tr>
|
||||||
|
<tr><td class="key">Expiration Datestamp</td>
|
||||||
|
<td class="value"><xsl:value-of select="@expirationDate"/></td></tr>
|
||||||
|
<tr><td class="key">Resumption Token</td>
|
||||||
|
<td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</table>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Unknown Metadata
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai:metadata/*" priority='-100'>
|
||||||
|
<h4>Metadata Format <em><xsl:value-of select="$metadataPrefix"/></em></h4>
|
||||||
|
<div class="xmlSource">
|
||||||
|
<xsl:apply-templates select="." mode='xmlMarkup' />
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
DublinCore Metadata
|
||||||
|
-->
|
||||||
|
<xsl:template match="oai_dc:dc" xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/">
|
||||||
|
<h4>Metadata Format <em>DublinCore</em></h4>
|
||||||
|
<table>
|
||||||
|
<xsl:apply-templates select="*" />
|
||||||
|
</table>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:title" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Title</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:creator" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Author or Creator</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:subject" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Subject and Keywords</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:description" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Description</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:publisher" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Publisher</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:contributor" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Other Contributor</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:date" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Date</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:type" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Resource Type</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:format" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Format</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:identifier" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Resource Identifier</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:source" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Source</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:language" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Language</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:relation" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Relation</td><td class="value">
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test='starts-with(.,"http")'>
|
||||||
|
<a href="{.}"><xsl:value-of select="."/></a>
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:otherwise>
|
||||||
|
<xsl:value-of select="."/>
|
||||||
|
</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
</td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:coverage" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Coverage</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="dc:rights" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<tr><td class="key">Rights Management</td><td class="value"><xsl:value-of select="."/></td></tr>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
XML Pretty Maker
|
||||||
|
-->
|
||||||
|
<xsl:template match="node()" mode='xmlMarkup'>
|
||||||
|
<div class="xmlBlock">
|
||||||
|
<<span class="xmlTagName"><xsl:value-of select='name(.)' /></span><xsl:apply-templates select="@*" mode='xmlMarkup'/>><xsl:apply-templates select="node()" mode='xmlMarkup' /></<span class="xmlTagName"><xsl:value-of select='name(.)' /></span>>
|
||||||
|
</div>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="text()" mode='xmlMarkup'><span class="xmlText"><xsl:value-of select='.' /></span></xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="@*" mode='xmlMarkup'>
|
||||||
|
<xsl:text> </xsl:text><span class="xmlAttrName"><xsl:value-of select='name()' /></span>="<span class="xmlAttrValue"><xsl:value-of select='.' /></span>"
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
</xsl:stylesheet>
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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 OCC\OaiPmh2\Middleware\Dispatcher;
|
||||||
|
use OCC\PSR15\QueueRequestHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main application of the OAI-PMH 2.0 Data Provider.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class App
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The PSR-15 Server Request Handler.
|
||||||
|
*/
|
||||||
|
protected QueueRequestHandler $requestHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate application.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->requestHandler = new QueueRequestHandler([new Dispatcher()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->requestHandler->handle();
|
||||||
|
$this->requestHandler->respond();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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 OCC\Basics\Traits\Singleton;
|
||||||
|
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
|
||||||
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads, validates and provides configuration settings.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*
|
||||||
|
* @property-read string $repositoryName
|
||||||
|
* @property-read string $adminEmail
|
||||||
|
* @property-read string $database
|
||||||
|
* @property-read array $metadataPrefix
|
||||||
|
* @property-read int $maxRecords
|
||||||
|
* @property-read int $tokenValid
|
||||||
|
*
|
||||||
|
* @template TKey of string
|
||||||
|
* @template TValue
|
||||||
|
*/
|
||||||
|
class Configuration
|
||||||
|
{
|
||||||
|
use Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified path to the configuration file.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected const CONFIG_FILE = __DIR__ . '/../config/config.yml';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration settings.
|
||||||
|
*
|
||||||
|
* @var array<TKey, TValue>
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
])
|
||||||
|
])
|
||||||
|
],
|
||||||
|
'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|ValidationFailedException
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$config = Yaml::parseFile($configPath);
|
||||||
|
$validator = Validation::createValidator();
|
||||||
|
$violations = $validator->validate($config, $this->getValidationConstraints());
|
||||||
|
if ($violations->count() > 0) {
|
||||||
|
throw new ValidationFailedException(null, $violations);
|
||||||
|
}
|
||||||
|
/** @var array<TKey, TValue> $config */
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and validate configuration settings from YAML file.
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException|ValidationFailedException
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->settings = $this->loadConfigFile();
|
||||||
|
} catch (FileNotFoundException|ValidationFailedException $exception) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic getter for $this->settings.
|
||||||
|
*
|
||||||
|
* @param TKey $name The setting to retrieve
|
||||||
|
*
|
||||||
|
* @return TValue|null The setting or NULL
|
||||||
|
*/
|
||||||
|
public function __get(string $name): mixed
|
||||||
|
{
|
||||||
|
return $this->settings[$name] ?? null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Console;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a record in the database.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'oai:records:add',
|
||||||
|
description: 'Add or update a record in the database'
|
||||||
|
)]
|
||||||
|
class AddRecordCommand extends Command
|
||||||
|
{
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Console;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update records in database from CSV file.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'oai:records:bulk-update',
|
||||||
|
description: 'Update records in database from CSV file'
|
||||||
|
)]
|
||||||
|
class BulkUpdateCommand extends Command
|
||||||
|
{
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
// https://symfony.com/doc/current/console/input.html
|
||||||
|
// https://symfony.com/doc/current/components/serializer.html#the-csvencoder
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Console;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a record from database.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'oai:records:delete',
|
||||||
|
description: 'Delete a record from database'
|
||||||
|
)]
|
||||||
|
class DeleteRecordCommand extends Command
|
||||||
|
{
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Console;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune expired resumption tokens from database.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'oai:tokens:prune',
|
||||||
|
description: 'Prune expired resumption tokens from database'
|
||||||
|
)]
|
||||||
|
class PruneResumptionTokensCommand extends Command
|
||||||
|
{
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$expired = Database::getInstance()->pruneResumptionTokens();
|
||||||
|
$output->writeln([
|
||||||
|
'',
|
||||||
|
sprintf(
|
||||||
|
' [OK] %d resumption tokens are expired and were successfully deleted. ',
|
||||||
|
$expired
|
||||||
|
),
|
||||||
|
''
|
||||||
|
]);
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Console;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\NullOutput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize metadata formats in database with configuration.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'oai:formats:update',
|
||||||
|
description: 'Update metadata formats in database from configuration'
|
||||||
|
)]
|
||||||
|
class UpdateFormatsCommand extends Command
|
||||||
|
{
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$formats = Configuration::getInstance()->metadataPrefix;
|
||||||
|
$inDatabase = Database::getInstance()->getMetadataFormats()->getQueryResult();
|
||||||
|
$added = 0;
|
||||||
|
$deleted = 0;
|
||||||
|
foreach ($formats as $prefix => $format) {
|
||||||
|
if (in_array($prefix, array_keys($inDatabase), true)) {
|
||||||
|
if (
|
||||||
|
$format['namespace'] === $inDatabase[$prefix]->getNamespace()
|
||||||
|
and $format['schema'] === $inDatabase[$prefix]->getSchema()
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Database::getInstance()->addOrUpdateMetadataFormat($prefix, $format['namespace'], $format['schema'])) {
|
||||||
|
++$added;
|
||||||
|
$output->writeln([
|
||||||
|
sprintf(
|
||||||
|
' [OK] Metadata format "%s" added or updated successfully. ',
|
||||||
|
$prefix
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$output->writeln([
|
||||||
|
sprintf(
|
||||||
|
' [ERROR] Could not add or update metadata format "%s". ',
|
||||||
|
$prefix
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (array_keys($inDatabase) as $prefix) {
|
||||||
|
if (!in_array($prefix, array_keys($formats), true)) {
|
||||||
|
if (Database::getInstance()->removeMetadataFormat($prefix)) {
|
||||||
|
++$deleted;
|
||||||
|
$output->writeln([
|
||||||
|
sprintf(
|
||||||
|
' [OK] Metadata format "%s" and all associated records deleted successfully. ',
|
||||||
|
$prefix
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$output->writeln([
|
||||||
|
sprintf(
|
||||||
|
' [ERROR] Could not delete metadata format "%s". ',
|
||||||
|
$prefix
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** @var Application */
|
||||||
|
$app = $this->getApplication();
|
||||||
|
$app->doRun(
|
||||||
|
new ArrayInput([
|
||||||
|
'command' => 'orm:clear-cache:result',
|
||||||
|
'--flush' => true
|
||||||
|
]),
|
||||||
|
new NullOutput()
|
||||||
|
);
|
||||||
|
$currentFormats = array_keys(Database::getInstance()->getMetadataFormats()->getQueryResult());
|
||||||
|
if (count($currentFormats) > 0) {
|
||||||
|
$output->writeln(
|
||||||
|
[
|
||||||
|
'',
|
||||||
|
' The following metadata formats are currently supported: ',
|
||||||
|
' ======================================================= ',
|
||||||
|
'',
|
||||||
|
' "' . implode('", "', $currentFormats) . '" ',
|
||||||
|
''
|
||||||
|
],
|
||||||
|
1 | 16
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$output->writeln(
|
||||||
|
[
|
||||||
|
'',
|
||||||
|
' [INFO] There are currently no metadata formats supported. ',
|
||||||
|
' Please add a metadata prefix to config/config.yml and run ',
|
||||||
|
' command "php bin/cli oai:formats:update" again! ',
|
||||||
|
''
|
||||||
|
],
|
||||||
|
1 | 16
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,441 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\EntityManager;
|
||||||
|
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||||
|
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||||
|
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||||
|
use Exception;
|
||||||
|
use OCC\Basics\Traits\Singleton;
|
||||||
|
use OCC\OaiPmh2\Database\Format;
|
||||||
|
use OCC\OaiPmh2\Database\Record;
|
||||||
|
use OCC\OaiPmh2\Database\Result;
|
||||||
|
use OCC\OaiPmh2\Database\Set;
|
||||||
|
use OCC\OaiPmh2\Database\Token;
|
||||||
|
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||||
|
use Symfony\Component\Filesystem\Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all database shenanigans.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*
|
||||||
|
* @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 string $prefix The metadata prefix
|
||||||
|
* @param string $namespace The namespace URI
|
||||||
|
* @param string $schema The schema URL
|
||||||
|
*
|
||||||
|
* @return bool Whether the format was inserted/updated successfully
|
||||||
|
*/
|
||||||
|
public function addOrUpdateMetadataFormat(string $prefix, string $namespace, string $schema): bool
|
||||||
|
{
|
||||||
|
$inDatabase = $this->getMetadataFormats()->getQueryResult();
|
||||||
|
if (in_array($prefix, array_keys($inDatabase), true)) {
|
||||||
|
try {
|
||||||
|
$dql = $this->entityManager->createQueryBuilder();
|
||||||
|
$dql->update(Format::class, 'format')
|
||||||
|
->set('format.namespace', ':namepsace')
|
||||||
|
->set('format.xmlSchema', ':schema')
|
||||||
|
->where($dql->expr()->eq('format.prefix', ':prefix'))
|
||||||
|
->setParameter('prefix', $prefix)
|
||||||
|
->setParameter('namespace', $namespace)
|
||||||
|
->setParameter('schema', $schema);
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
$query->execute();
|
||||||
|
return true;
|
||||||
|
} catch (Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$format = new Format($prefix, $namespace, $schema);
|
||||||
|
$this->entityManager->persist($format);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
return true;
|
||||||
|
} catch (Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('record')
|
||||||
|
->from(Record::class, 'record')
|
||||||
|
->orderBy('record.lastChanged', 'ASC')
|
||||||
|
->setMaxResults(1);
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
$query->enableResultCache();
|
||||||
|
/** @var ?array<string, \DateTime> */
|
||||||
|
$result = $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY);
|
||||||
|
if (isset($result)) {
|
||||||
|
$timestamp = $result['lastChanged']->format('Y-m-d\TH:i:s\Z');
|
||||||
|
}
|
||||||
|
return $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(
|
||||||
|
'format.records',
|
||||||
|
'records',
|
||||||
|
'WITH',
|
||||||
|
$dql->expr()->andX(
|
||||||
|
$dql->expr()->eq('records.identifier', ':identifier'),
|
||||||
|
$dql->expr()->neq('records.data', '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->setParameter('identifier', $identifier);
|
||||||
|
}
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
$query->enableResultCache();
|
||||||
|
/** @var Formats */
|
||||||
|
$queryResult = $query->getResult();
|
||||||
|
return new Result($queryResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single record.
|
||||||
|
*
|
||||||
|
* @param string $identifier The record identifier
|
||||||
|
* @param string $metadataPrefix The metadata prefix
|
||||||
|
*
|
||||||
|
* @return ?Record The record or NULL on failure
|
||||||
|
*/
|
||||||
|
public function getRecord(string $identifier, string $metadataPrefix): ?Record
|
||||||
|
{
|
||||||
|
$dql = $this->entityManager->createQueryBuilder();
|
||||||
|
$dql->select('record')
|
||||||
|
->from(Record::class, 'record')
|
||||||
|
->where($dql->expr()->eq('record.identifier', ':identifier'))
|
||||||
|
->andWhere($dql->expr()->eq('record.format', ':format'))
|
||||||
|
->setParameter('identifier', $identifier)
|
||||||
|
->setParameter('format', $metadataPrefix)
|
||||||
|
->setMaxResults(1);
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
/** @var ?Record */
|
||||||
|
return $query->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Result<Records> The records and possibly a resumtion token
|
||||||
|
*/
|
||||||
|
public function getRecords(
|
||||||
|
string $verb,
|
||||||
|
string $metadataPrefix,
|
||||||
|
int $counter = 0,
|
||||||
|
?string $from = null,
|
||||||
|
?string $until = null,
|
||||||
|
?string $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', new DateTime($from));
|
||||||
|
}
|
||||||
|
if (isset($until)) {
|
||||||
|
$dql->andWhere($dql->expr()->lte('record.lastChanged', ':until'));
|
||||||
|
$dql->setParameter('until', new DateTime($until));
|
||||||
|
}
|
||||||
|
if (isset($set)) {
|
||||||
|
$dql->andWhere($dql->expr()->in('record.sets', ':set'));
|
||||||
|
$dql->setParameter('set', $set);
|
||||||
|
}
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
/** @var Records */
|
||||||
|
$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,
|
||||||
|
'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($counter = 0): Result
|
||||||
|
{
|
||||||
|
$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 */
|
||||||
|
$resultQuery = $query->getResult();
|
||||||
|
$result = new Result($resultQuery);
|
||||||
|
$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('COUNT(record.identifier)')
|
||||||
|
->from(Record::class, 'record')
|
||||||
|
->where($dql->expr()->eq('record.identifier', ':identifier'))
|
||||||
|
->setParameter('identifier', $identifier)
|
||||||
|
->setMaxResults(1);
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
return (bool) $query->getOneOrNullResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prune expired resumption tokens.
|
||||||
|
*
|
||||||
|
* @return int The number of deleted tokens
|
||||||
|
*/
|
||||||
|
public function pruneResumptionTokens(): int
|
||||||
|
{
|
||||||
|
$dql = $this->entityManager->createQueryBuilder();
|
||||||
|
$dql->delete(Token::class, 'token')
|
||||||
|
->where($dql->expr()->lt('token.validUntil', ':now'))
|
||||||
|
->setParameter('now', new DateTime());
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
/** @var int */
|
||||||
|
return $query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove metadata format and all associated records.
|
||||||
|
*
|
||||||
|
* @param string $prefix The metadata prefix
|
||||||
|
*
|
||||||
|
* @return bool TRUE on success or FALSE on failure
|
||||||
|
*/
|
||||||
|
public function removeMetadataFormat(string $prefix): bool
|
||||||
|
{
|
||||||
|
$dql = $this->entityManager->createQueryBuilder();
|
||||||
|
$dql->delete(Format::class, 'format')
|
||||||
|
->where($dql->expr()->eq('format.prefix', ':prefix'))
|
||||||
|
->setParameter('prefix', $prefix);
|
||||||
|
$query = $dql->getQuery();
|
||||||
|
try {
|
||||||
|
$query->execute();
|
||||||
|
return true;
|
||||||
|
} catch (Exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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__ . '/Database'])
|
||||||
|
);
|
||||||
|
$configuration->setProxyDir(__DIR__ . '/../var/generated');
|
||||||
|
$configuration->setProxyNamespace('OCC\OaiPmh2\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',
|
||||||
|
'postgres' => 'pdo_pgsql',
|
||||||
|
'sqlite' => 'pdo_sqlite'
|
||||||
|
]);
|
||||||
|
$connection = DriverManager::getConnection($parser->parse($dsn), $configuration);
|
||||||
|
|
||||||
|
$this->entityManager = new EntityManager($connection, $configuration);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Database;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Entity for formats.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'formats')]
|
||||||
|
class Format
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The unique metadata prefix.
|
||||||
|
*/
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $prefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format's namespace URI.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $namespace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format's schema URL.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $xmlSchema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of associated records.
|
||||||
|
*
|
||||||
|
* @var Collection<int, Record>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Record::class, mappedBy: 'format')]
|
||||||
|
private Collection $records;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bi-directional association with records.
|
||||||
|
*
|
||||||
|
* @param Record $record The record to add to this format
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addRecord(Record $record): void
|
||||||
|
{
|
||||||
|
if (!$this->records->contains($record)) {
|
||||||
|
$this->records->add($record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the format's namespace URI.
|
||||||
|
*
|
||||||
|
* @return string The namespace URI
|
||||||
|
*/
|
||||||
|
public function getNamespace(): string
|
||||||
|
{
|
||||||
|
return $this->namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the metadata prefix for this format.
|
||||||
|
*
|
||||||
|
* @return string The metadata prefix
|
||||||
|
*/
|
||||||
|
public function getPrefix(): string
|
||||||
|
{
|
||||||
|
return $this->prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of associated records.
|
||||||
|
*
|
||||||
|
* @return Collection<int, Record> The associated records
|
||||||
|
*/
|
||||||
|
public function getRecords(): Collection
|
||||||
|
{
|
||||||
|
return $this->records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the format's schema URL.
|
||||||
|
*
|
||||||
|
* @return string The schema URL
|
||||||
|
*/
|
||||||
|
public function getSchema(): string
|
||||||
|
{
|
||||||
|
return $this->xmlSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bi-directional association with records.
|
||||||
|
*
|
||||||
|
* @param Record $record The record to remove from this metadata prefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function removeRecord(Record $record): void
|
||||||
|
{
|
||||||
|
$this->records->removeElement($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate namespace and schema URLs.
|
||||||
|
*
|
||||||
|
* @param string $url The namespace or schema URL
|
||||||
|
*
|
||||||
|
* @return string The validated URL
|
||||||
|
*
|
||||||
|
* @throws ValidationFailedException
|
||||||
|
*/
|
||||||
|
protected function validate(string $url): string
|
||||||
|
{
|
||||||
|
$validator = Validation::createValidator();
|
||||||
|
$violations = $validator->validate($url, new Assert\Url());
|
||||||
|
if ($violations->count() > 0) {
|
||||||
|
throw new ValidationFailedException(null, $violations);
|
||||||
|
}
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get new entity of format.
|
||||||
|
*
|
||||||
|
* @param string $prefix The metadata prefix
|
||||||
|
* @param string $namespace The format's namespace URI
|
||||||
|
* @param string $schema The format's schema URL
|
||||||
|
*
|
||||||
|
* @throws ValidationFailedException
|
||||||
|
*/
|
||||||
|
public function __construct(string $prefix, string $namespace, string $schema)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->prefix = $prefix;
|
||||||
|
$this->namespace = $this->validate($namespace);
|
||||||
|
$this->xmlSchema = $this->validate($schema);
|
||||||
|
$this->records = new ArrayCollection();
|
||||||
|
} catch (ValidationFailedException $exception) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Database;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Exception\ValidationFailedException;
|
||||||
|
use Symfony\Component\Validator\Validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Entity for records.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'records')]
|
||||||
|
class Record
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The record identifier.
|
||||||
|
*/
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $identifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The associated format.
|
||||||
|
*/
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\ManyToOne(targetEntity: Format::class, inversedBy: 'records')]
|
||||||
|
#[ORM\JoinColumn(name: 'format', referencedColumnName: 'prefix')]
|
||||||
|
private Format $format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date and time of last change.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(name: 'last_changed', type: 'datetime')]
|
||||||
|
private DateTime $lastChanged;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The record's content.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'text')]
|
||||||
|
private string $content = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of associated sets.
|
||||||
|
*
|
||||||
|
* @var Collection<int, Set>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Set::class, inversedBy: 'records', indexBy: 'spec')]
|
||||||
|
#[ORM\JoinTable(name: 'records_sets')]
|
||||||
|
#[ORM\JoinColumn(name: 'record_identifier', referencedColumnName: 'identifier')]
|
||||||
|
#[ORM\JoinColumn(name: 'record_format', referencedColumnName: 'format')]
|
||||||
|
#[ORM\InverseJoinColumn(name: 'set_spec', referencedColumnName: 'spec')]
|
||||||
|
private Collection $sets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the record's content.
|
||||||
|
*
|
||||||
|
* @return string The record's content
|
||||||
|
*/
|
||||||
|
public function getContent(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the record identifier.
|
||||||
|
*
|
||||||
|
* @return string The record identifier
|
||||||
|
*/
|
||||||
|
public function getIdentifier(): string
|
||||||
|
{
|
||||||
|
return $this->identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bi-directional association with format.
|
||||||
|
*
|
||||||
|
* @param Format $format The metadata prefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function addFormat(Format $format): void
|
||||||
|
{
|
||||||
|
$this->format = $format;
|
||||||
|
$format->addRecord($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate the record with a set.
|
||||||
|
*
|
||||||
|
* @param Set $set The set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addSet(Set $set): void
|
||||||
|
{
|
||||||
|
if (!$this->sets->contains($set)) {
|
||||||
|
$this->sets->add($set);
|
||||||
|
$set->addRecord($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the associated format.
|
||||||
|
*
|
||||||
|
* @return Format The associated format
|
||||||
|
*/
|
||||||
|
public function getFormat(): Format
|
||||||
|
{
|
||||||
|
return $this->format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the date and time of last change.
|
||||||
|
*
|
||||||
|
* @return DateTime The datetime of last change
|
||||||
|
*/
|
||||||
|
public function getLastChanged(): DateTime
|
||||||
|
{
|
||||||
|
return $this->lastChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of associated sets.
|
||||||
|
*
|
||||||
|
* @return Collection<int, Set> The associated sets
|
||||||
|
*/
|
||||||
|
public function getSets(): Collection
|
||||||
|
{
|
||||||
|
return $this->sets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove record from set.
|
||||||
|
*
|
||||||
|
* @param Set $set The set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function removeSet(Set $set): void
|
||||||
|
{
|
||||||
|
if ($this->sets->contains($set)) {
|
||||||
|
$this->sets->removeElement($set);
|
||||||
|
$set->removeRecord($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set record's content.
|
||||||
|
*
|
||||||
|
* @param string $data The record's content
|
||||||
|
* @param bool $validate Should the input be validated?
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws ValidationFailedException
|
||||||
|
*/
|
||||||
|
public function setContent(string $data, bool $validate = true): void
|
||||||
|
{
|
||||||
|
$data = trim($data);
|
||||||
|
if ($validate && $data !== '') {
|
||||||
|
try {
|
||||||
|
$data = $this->validate($data);
|
||||||
|
} catch (ValidationFailedException $exception) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->content = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set date and time of last change.
|
||||||
|
*
|
||||||
|
* @param ?DateTime $dateTime The datetime of last change or NULL for "NOW"
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setLastChanged(?DateTime $dateTime = null): void
|
||||||
|
{
|
||||||
|
if (!isset($dateTime)) {
|
||||||
|
$dateTime = new DateTime();
|
||||||
|
}
|
||||||
|
$this->lastChanged = $dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate XML content.
|
||||||
|
*
|
||||||
|
* @param string $xml The XML string
|
||||||
|
*
|
||||||
|
* @return string The validated XML string
|
||||||
|
*
|
||||||
|
* @throws ValidationFailedException
|
||||||
|
*/
|
||||||
|
protected function validate(string $xml): string
|
||||||
|
{
|
||||||
|
$validator = Validation::createValidator();
|
||||||
|
$violations = $validator->validate($xml, new Assert\Type('string'));
|
||||||
|
if ($violations->count() > 0) {
|
||||||
|
throw new ValidationFailedException(null, $violations);
|
||||||
|
}
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get new entity of record.
|
||||||
|
*
|
||||||
|
* @param string $identifier The record identifier
|
||||||
|
* @param Format $format The format
|
||||||
|
* @param string $data The record's content
|
||||||
|
*
|
||||||
|
* @throws ValidationFailedException
|
||||||
|
*/
|
||||||
|
public function __construct(string $identifier, Format $format, string $data = '')
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->identifier = $identifier;
|
||||||
|
$this->addFormat($format);
|
||||||
|
$this->setContent($data);
|
||||||
|
$this->setLastChanged();
|
||||||
|
$this->sets = new ArrayCollection();
|
||||||
|
} catch (ValidationFailedException $exception) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the record's content.
|
||||||
|
*
|
||||||
|
* @return string The record's content
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Database;
|
||||||
|
|
||||||
|
use Countable;
|
||||||
|
use Iterator;
|
||||||
|
use OCC\Basics\InterfaceTraits\Countable as CountableTrait;
|
||||||
|
use OCC\Basics\InterfaceTraits\Iterator as IteratorTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A database result set with optional resumption token.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*
|
||||||
|
* @template QueryResult of array<string, Format|Record|Set>
|
||||||
|
* @implements Iterator<QueryResult>
|
||||||
|
*/
|
||||||
|
class Result implements Countable, Iterator
|
||||||
|
{
|
||||||
|
use CountableTrait;
|
||||||
|
use IteratorTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the Doctrine result set.
|
||||||
|
*
|
||||||
|
* @var QueryResult
|
||||||
|
*/
|
||||||
|
private array $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the optional resumption token.
|
||||||
|
*/
|
||||||
|
protected ?Token $resumptionToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query result.
|
||||||
|
*
|
||||||
|
* @return QueryResult The result set
|
||||||
|
*/
|
||||||
|
public function getQueryResult(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resumption token.
|
||||||
|
*
|
||||||
|
* @return ?Token The resumption token or NULL if not applicable
|
||||||
|
*/
|
||||||
|
public function getResumptionToken(): ?Token
|
||||||
|
{
|
||||||
|
return $this->resumptionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the resumption token.
|
||||||
|
*
|
||||||
|
* @param Token $token The resumption token
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setResumptionToken(Token $token): void
|
||||||
|
{
|
||||||
|
$this->resumptionToken = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new result set.
|
||||||
|
*
|
||||||
|
* @param QueryResult $queryResult The Doctrine result set
|
||||||
|
*/
|
||||||
|
public function __construct(array $queryResult)
|
||||||
|
{
|
||||||
|
$this->data = $queryResult;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Database;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Entity for sets.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'sets')]
|
||||||
|
class Set
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The unique set spec.
|
||||||
|
*/
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $spec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the set.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A description of the set.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'text')]
|
||||||
|
private string $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of associated records.
|
||||||
|
*
|
||||||
|
* @var Collection<int, Record>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Record::class, mappedBy: 'sets')]
|
||||||
|
private Collection $records;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bi-directional association with records.
|
||||||
|
*
|
||||||
|
* @param Record $record The record to add to this set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addRecord(Record $record): void
|
||||||
|
{
|
||||||
|
if (!$this->records->contains($record)) {
|
||||||
|
$this->records->add($record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the description of this set.
|
||||||
|
*
|
||||||
|
* @return string The set description
|
||||||
|
*/
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of this set.
|
||||||
|
*
|
||||||
|
* @return string The set name
|
||||||
|
*/
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the set spec.
|
||||||
|
*
|
||||||
|
* @return string The set spec
|
||||||
|
*/
|
||||||
|
public function getSpec(): string
|
||||||
|
{
|
||||||
|
return $this->spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a collection of associated records.
|
||||||
|
*
|
||||||
|
* @return Collection<int, Record> The associated records
|
||||||
|
*/
|
||||||
|
public function getRecords(): Collection
|
||||||
|
{
|
||||||
|
return $this->records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bi-directional association with records.
|
||||||
|
*
|
||||||
|
* @param Record $record The record to remove from this set
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function removeRecord(Record $record): void
|
||||||
|
{
|
||||||
|
$this->records->removeElement($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the description for this set.
|
||||||
|
*
|
||||||
|
* @param string $description The description
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setDescription(string $description): void
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get new entity of set.
|
||||||
|
*
|
||||||
|
* @param string $spec The set spec
|
||||||
|
* @param string $name The name of the set
|
||||||
|
* @param string $description The description of the set
|
||||||
|
*/
|
||||||
|
public function __construct(string $spec, string $name, string $description = '')
|
||||||
|
{
|
||||||
|
$this->spec = $spec;
|
||||||
|
$this->name = $name;
|
||||||
|
$this->setDescription($description);
|
||||||
|
$this->records = new ArrayCollection();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Database;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doctrine/ORM Entity for resumption tokens.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'tokens')]
|
||||||
|
class Token
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The resumption token.
|
||||||
|
*/
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verb for which the token is issued.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $verb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query parameters as serialized array.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(type: 'string')]
|
||||||
|
private string $parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date and time of validity.
|
||||||
|
*/
|
||||||
|
#[ORM\Column(name: 'valid_until', type: 'datetime')]
|
||||||
|
private DateTime $validUntil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resumption token.
|
||||||
|
*
|
||||||
|
* @return string The resumption token
|
||||||
|
*/
|
||||||
|
public function getToken(): string
|
||||||
|
{
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query parameters.
|
||||||
|
*
|
||||||
|
* @return array<string, int|string|null> The query parameters
|
||||||
|
*/
|
||||||
|
public function getParameters(): array
|
||||||
|
{
|
||||||
|
/** @var array<string, int|string|null> */
|
||||||
|
return unserialize($this->parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the date and time of validity.
|
||||||
|
*
|
||||||
|
* @return DateTime The datetime of validity
|
||||||
|
*/
|
||||||
|
public function getValidUntil(): DateTime
|
||||||
|
{
|
||||||
|
return $this->validUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the verb for which the token was issued.
|
||||||
|
*
|
||||||
|
* @return string The verb
|
||||||
|
*/
|
||||||
|
public function getVerb(): string
|
||||||
|
{
|
||||||
|
return $this->verb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get new entity of resumption token.
|
||||||
|
*
|
||||||
|
* @param string $verb The verb for which the token is issued
|
||||||
|
* @param array<string, int|string|null> $parameters The query parameters
|
||||||
|
*/
|
||||||
|
public function __construct(string $verb, array $parameters)
|
||||||
|
{
|
||||||
|
$this->token = substr(md5(microtime()), 0, 8);
|
||||||
|
$this->verb = $verb;
|
||||||
|
$this->parameters = serialize($parameters);
|
||||||
|
$validity = new DateTime();
|
||||||
|
$validity->add(new DateInterval('PT' . Configuration::getInstance()->tokenValid . 'S'));
|
||||||
|
$this->validUntil = $validity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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 DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use DOMException;
|
||||||
|
use DOMNode;
|
||||||
|
use GuzzleHttp\Psr7\Uri;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OAI-PMH XML response object.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class Document
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This holds the DOMDocument of the OAI-PMH XML response.
|
||||||
|
*/
|
||||||
|
protected DOMDocument $dom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds the root node of the OAI-PMH XML response.
|
||||||
|
*/
|
||||||
|
protected DOMElement $rootNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new XML element.
|
||||||
|
*
|
||||||
|
* @param string $localName The local name for the element
|
||||||
|
* @param string $value The optional value for the element
|
||||||
|
* @param bool $appendToRoot Append the new element to the root node?
|
||||||
|
*
|
||||||
|
* @return DOMElement The newly created element
|
||||||
|
*/
|
||||||
|
public function createElement(string $localName, string $value = '', bool $appendToRoot = false): DOMElement
|
||||||
|
{
|
||||||
|
$node = $this->dom->createElement(
|
||||||
|
$localName,
|
||||||
|
htmlspecialchars($value, ENT_XML1, 'UTF-8')
|
||||||
|
);
|
||||||
|
if ($appendToRoot) {
|
||||||
|
$this->rootNode->appendChild($node);
|
||||||
|
}
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import XML data into response document.
|
||||||
|
*
|
||||||
|
* @param string $data The XML data
|
||||||
|
*
|
||||||
|
* @return DOMNode The imported XML node
|
||||||
|
*
|
||||||
|
* @throws DOMException
|
||||||
|
*/
|
||||||
|
public function importData(string $data): DOMNode
|
||||||
|
{
|
||||||
|
$document = new DOMDocument('1.0', 'UTF-8');
|
||||||
|
$document->preserveWhiteSpace = false;
|
||||||
|
if ($document->loadXML($data) === true) {
|
||||||
|
/** @var DOMElement */
|
||||||
|
$rootNode = $document->documentElement;
|
||||||
|
$node = $this->dom->importNode($rootNode, true);
|
||||||
|
return $node;
|
||||||
|
} else {
|
||||||
|
throw new DOMException(
|
||||||
|
'Could not import the XML data. Most likely it is not well-formed.',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an OAI-PMH XML response.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $serverRequest The PSR-7 HTTP Server Request
|
||||||
|
*/
|
||||||
|
public function __construct(ServerRequestInterface $serverRequest)
|
||||||
|
{
|
||||||
|
$uri = $serverRequest->getUri();
|
||||||
|
|
||||||
|
// Create XML document.
|
||||||
|
$this->dom = new DOMDocument('1.0', 'UTF-8');
|
||||||
|
$this->dom->preserveWhiteSpace = false;
|
||||||
|
|
||||||
|
// Add processing instructions.
|
||||||
|
$basePath = $uri->getPath();
|
||||||
|
if (str_ends_with($basePath, 'index.php')) {
|
||||||
|
$basePath = pathinfo($basePath, PATHINFO_DIRNAME);
|
||||||
|
}
|
||||||
|
$stylesheet = Uri::composeComponents(
|
||||||
|
$uri->getScheme(),
|
||||||
|
$uri->getAuthority(),
|
||||||
|
rtrim($basePath, '/') . '/resources/stylesheet.xsl',
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
$xslt = $this->dom->createProcessingInstruction(
|
||||||
|
'xml-stylesheet',
|
||||||
|
sprintf(
|
||||||
|
'type="text/xsl" href="%s"',
|
||||||
|
$stylesheet
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->dom->appendChild($xslt);
|
||||||
|
|
||||||
|
// Add root element "OAI-PMH".
|
||||||
|
$root = $this->dom->createElement('OAI-PMH');
|
||||||
|
$this->dom->appendChild($root);
|
||||||
|
$root->setAttribute(
|
||||||
|
'xmlns',
|
||||||
|
'http://www.openarchives.org/OAI/2.0/'
|
||||||
|
);
|
||||||
|
$root->setAttribute(
|
||||||
|
'xmlns:xsi',
|
||||||
|
'http://www.w3.org/2001/XMLSchema-instance'
|
||||||
|
);
|
||||||
|
$root->setAttribute(
|
||||||
|
'xsi:schemaLocation',
|
||||||
|
'http://www.openarchives.org/OAI/2.0/ https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add element "responseDate".
|
||||||
|
$responseDate = $this->dom->createElement('responseDate', gmdate('Y-m-d\TH:i:s\Z'));
|
||||||
|
$root->appendChild($responseDate);
|
||||||
|
|
||||||
|
// Add element "request".
|
||||||
|
$baseUrl = Uri::composeComponents(
|
||||||
|
$uri->getScheme(),
|
||||||
|
$uri->getAuthority(),
|
||||||
|
$uri->getPath(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
$request = $this->dom->createElement('request', $baseUrl);
|
||||||
|
$root->appendChild($request);
|
||||||
|
foreach ($serverRequest->getAttributes() as $param => $value) {
|
||||||
|
$request->setAttribute(
|
||||||
|
$param,
|
||||||
|
htmlspecialchars($value, ENT_XML1 | ENT_COMPAT, 'UTF-8')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rootNode = $root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the OAI-PMH XML response.
|
||||||
|
*
|
||||||
|
* @return string The XML output
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$this->dom->formatOutput = true;
|
||||||
|
return (string) $this->dom->saveXML();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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 GuzzleHttp\Psr7\Utils;
|
||||||
|
use OCC\OaiPmh2\Middleware\ErrorHandler;
|
||||||
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all OAI-PMH requests.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
abstract class Middleware extends AbstractMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This holds the prepared response document.
|
||||||
|
*/
|
||||||
|
protected Document $preparedResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare response document.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The pre-processed request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
abstract protected function prepareResponse(ServerRequestInterface $request): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming server request.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming server request
|
||||||
|
*
|
||||||
|
* @return ServerRequestInterface The processed server request
|
||||||
|
*/
|
||||||
|
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
||||||
|
{
|
||||||
|
$this->prepareResponse($request);
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming response before.
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The incoming response
|
||||||
|
*
|
||||||
|
* @return ResponseInterface The processed response
|
||||||
|
*/
|
||||||
|
protected function processResponse(ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!ErrorHandler::getInstance()->hasErrors() && isset($this->preparedResponse)) {
|
||||||
|
$response = $response->withBody(Utils::streamFor((string) $this->preparedResponse));
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and dispatch a OAI-PMH server request.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class Dispatcher extends AbstractMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List of defined OAI-PMH parameters.
|
||||||
|
*/
|
||||||
|
protected const OAI_PARAMS = [
|
||||||
|
'verb',
|
||||||
|
'identifier',
|
||||||
|
'metadataPrefix',
|
||||||
|
'from',
|
||||||
|
'until',
|
||||||
|
'set',
|
||||||
|
'resumptionToken'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server request populated with request attributes.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The GET or POST request
|
||||||
|
*
|
||||||
|
* @return ServerRequestInterface The same request with parsed attributes
|
||||||
|
*/
|
||||||
|
protected function getRequestWithAttributes(ServerRequestInterface $request): ServerRequestInterface
|
||||||
|
{
|
||||||
|
$arguments = [];
|
||||||
|
if ($request->getMethod() === 'GET') {
|
||||||
|
$arguments = $request->getQueryParams();
|
||||||
|
} elseif ($request->getMethod() === 'POST') {
|
||||||
|
if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
|
||||||
|
$arguments = (array) $request->getParsedBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->validateArguments($arguments)) {
|
||||||
|
foreach ($arguments as $param => $value) {
|
||||||
|
$request = $request->withAttribute($param, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the OAI-PMH request.
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The request to dispatch
|
||||||
|
*
|
||||||
|
* @return ServerRequestInterface The processed server request
|
||||||
|
*/
|
||||||
|
protected function processRequest(ServerRequestInterface $request): ServerRequestInterface
|
||||||
|
{
|
||||||
|
$request = $this->getRequestWithAttributes($request);
|
||||||
|
if (!ErrorHandler::getInstance()->hasErrors()) {
|
||||||
|
/** @var Middleware $middleware */
|
||||||
|
$middleware = __NAMESPACE__ . '\\' . $request->getAttribute('verb');
|
||||||
|
$this->requestHandler->queue->enqueue(new $middleware());
|
||||||
|
}
|
||||||
|
$this->requestHandler->queue->enqueue(ErrorHandler::getInstance());
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize the OAI-PMH response.
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The response to finalize
|
||||||
|
*
|
||||||
|
* @return ResponseInterface The final response
|
||||||
|
*/
|
||||||
|
protected function processResponse(ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
// TODO: Add support for content compression
|
||||||
|
// https://openarchives.org/OAI/openarchivesprotocol.html#ResponseCompression
|
||||||
|
return $response->withHeader('Content-Type', 'text/xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the request parameters.
|
||||||
|
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ProtocolMessages
|
||||||
|
*
|
||||||
|
* @param string[] $arguments The request parameters
|
||||||
|
*
|
||||||
|
* @return bool Whether the parameters are syntactically valid
|
||||||
|
*/
|
||||||
|
protected function validateArguments(array $arguments): bool
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
count(array_diff(array_keys($arguments), self::OAI_PARAMS)) !== 0
|
||||||
|
or !isset($arguments['verb'])
|
||||||
|
) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
} else {
|
||||||
|
switch ($arguments['verb']) {
|
||||||
|
case 'GetRecord':
|
||||||
|
if (
|
||||||
|
count($arguments) !== 3
|
||||||
|
or !isset($arguments['identifier'])
|
||||||
|
or !isset($arguments['metadataPrefix'])
|
||||||
|
) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Identify':
|
||||||
|
if (count($arguments) !== 1) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ListIdentifiers':
|
||||||
|
case 'ListRecords':
|
||||||
|
if (
|
||||||
|
isset($arguments['metadataPrefix'])
|
||||||
|
xor isset($arguments['resumptionToken'])
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
(isset($arguments['resumptionToken']) && count($arguments) !== 2)
|
||||||
|
or isset($arguments['identifier'])
|
||||||
|
) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ListMetadataFormats':
|
||||||
|
if (count($arguments) !== 1) {
|
||||||
|
if (!isset($arguments['identifier']) || count($arguments) !== 2) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ListSets':
|
||||||
|
if (count($arguments) !== 1) {
|
||||||
|
if (!isset($arguments['resumptionToken']) || count($arguments) !== 2) {
|
||||||
|
ErrorHandler::getInstance()->withError('badArgument');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ErrorHandler::getInstance()->withError('badVerb');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !ErrorHandler::getInstance()->hasErrors();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
|
use GuzzleHttp\Psr7\Utils;
|
||||||
|
use OCC\Basics\Traits\Singleton;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\PSR15\AbstractMiddleware;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles OAI-PMH errors.
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class ErrorHandler extends AbstractMiddleware
|
||||||
|
{
|
||||||
|
use Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of defined OAI-PMH errors.
|
||||||
|
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ErrorConditions
|
||||||
|
*/
|
||||||
|
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.',
|
||||||
|
'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.',
|
||||||
|
'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.',
|
||||||
|
'noRecordsMatch' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.',
|
||||||
|
'noMetadataFormats' => 'There are no metadata formats available for the specified item.',
|
||||||
|
'noSetHierarchy' => 'The repository does not support sets.'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current error codes.
|
||||||
|
*
|
||||||
|
* @var string[] $errors
|
||||||
|
*/
|
||||||
|
protected array $errors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the response body.
|
||||||
|
*
|
||||||
|
* @return StreamInterface The response body stream
|
||||||
|
*/
|
||||||
|
protected function getResponseBody(): StreamInterface
|
||||||
|
{
|
||||||
|
$document = new Document($this->requestHandler->request);
|
||||||
|
foreach (array_unique($this->errors) as $errorCode) {
|
||||||
|
$error = $document->createElement('error', self::OAI_ERRORS[$errorCode], true);
|
||||||
|
$error->setAttribute('code', $errorCode);
|
||||||
|
}
|
||||||
|
return Utils::streamFor((string) $document);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if currently there are errors to handle.
|
||||||
|
*
|
||||||
|
* @return bool Whether the error handler has any errors registered
|
||||||
|
*/
|
||||||
|
public function hasErrors(): bool
|
||||||
|
{
|
||||||
|
return (bool) count($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an error response if errors occured.
|
||||||
|
*
|
||||||
|
* @param ResponseInterface $response The incoming response
|
||||||
|
*
|
||||||
|
* @return ResponseInterface The error response
|
||||||
|
*/
|
||||||
|
protected function processResponse(ResponseInterface $response): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($this->hasErrors()) {
|
||||||
|
$response = $response->withBody($this->getResponseBody());
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate an OAI-PMH error to the error handler.
|
||||||
|
*
|
||||||
|
* @param string $errorCode The error code to handle
|
||||||
|
*
|
||||||
|
* @return ErrorHandler The ErrorHandler instance
|
||||||
|
*
|
||||||
|
* @throws DomainException
|
||||||
|
*/
|
||||||
|
public function withError(string $errorCode): ErrorHandler
|
||||||
|
{
|
||||||
|
if (in_array($errorCode, array_keys(self::OAI_ERRORS), true)) {
|
||||||
|
$this->errors[] = $errorCode;
|
||||||
|
} else {
|
||||||
|
throw new DomainException(
|
||||||
|
sprintf(
|
||||||
|
'Valid OAI-PMH error code expected, "%s" given.',
|
||||||
|
$errorCode
|
||||||
|
),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a singleton class, thus the constructor is private.
|
||||||
|
*
|
||||||
|
* Usage: Get an instance by calling ErrorHandler::getInstance()
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "GetRecord" request.
|
||||||
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#GetRecord
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class GetRecord extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare the response body for verb "GetRecord".
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$params = $request->getAttributes();
|
||||||
|
$oaiRecord = Database::getInstance()->getRecord($params['identifier'], $params['metadataPrefix']);
|
||||||
|
|
||||||
|
if (!isset($oaiRecord)) {
|
||||||
|
if (Database::getInstance()->idDoesExist($params['identifier'])) {
|
||||||
|
ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
|
||||||
|
} else {
|
||||||
|
ErrorHandler::getInstance()->withError('idDoesNotExist');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new Document($request);
|
||||||
|
$getRecord = $document->createElement('GetRecord', '', true);
|
||||||
|
|
||||||
|
$record = $document->createElement('record');
|
||||||
|
$getRecord->appendChild($record);
|
||||||
|
|
||||||
|
$header = $document->createElement('header');
|
||||||
|
if ($oaiRecord->getContent() === '') {
|
||||||
|
$header->setAttribute('status', 'deleted');
|
||||||
|
}
|
||||||
|
$record->appendChild($header);
|
||||||
|
|
||||||
|
$identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
|
||||||
|
$header->appendChild($identifier);
|
||||||
|
|
||||||
|
$datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
|
||||||
|
$header->appendChild($datestamp);
|
||||||
|
|
||||||
|
foreach ($oaiRecord->getSets() as $set) {
|
||||||
|
$setSpec = $document->createElement('setSpec', $set->getName());
|
||||||
|
$header->appendChild($setSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($oaiRecord->getContent() !== '') {
|
||||||
|
$metadata = $document->createElement('metadata');
|
||||||
|
$record->appendChild($metadata);
|
||||||
|
|
||||||
|
$data = $document->importData($oaiRecord->getContent());
|
||||||
|
$metadata->appendChild($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preparedResponse = $document;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use GuzzleHttp\Psr7\Uri;
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "Identify" request.
|
||||||
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class Identify extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare the response body for verb "Identify".
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$document = new Document($request);
|
||||||
|
$identify = $document->createElement('Identify', '', true);
|
||||||
|
|
||||||
|
$name = Configuration::getInstance()->repositoryName;
|
||||||
|
$repositoryName = $document->createElement('repositoryName', $name);
|
||||||
|
$identify->appendChild($repositoryName);
|
||||||
|
|
||||||
|
$uri = Uri::composeComponents(
|
||||||
|
$request->getUri()->getScheme(),
|
||||||
|
$request->getUri()->getAuthority(),
|
||||||
|
$request->getUri()->getPath(),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
$baseURL = $document->createElement('baseURL', $uri);
|
||||||
|
$identify->appendChild($baseURL);
|
||||||
|
|
||||||
|
$protocolVersion = $document->createElement('protocolVersion', '2.0');
|
||||||
|
$identify->appendChild($protocolVersion);
|
||||||
|
|
||||||
|
$email = Configuration::getInstance()->adminEmail;
|
||||||
|
$adminEmail = $document->createElement('adminEmail', $email);
|
||||||
|
$identify->appendChild($adminEmail);
|
||||||
|
|
||||||
|
$datestamp = Database::getInstance()->getEarliestDatestamp();
|
||||||
|
$earliestDatestamp = $document->createElement('earliestDatestamp', $datestamp);
|
||||||
|
$identify->appendChild($earliestDatestamp);
|
||||||
|
|
||||||
|
$deletedRecord = $document->createElement('deletedRecord', 'transient');
|
||||||
|
$identify->appendChild($deletedRecord);
|
||||||
|
|
||||||
|
$granularity = $document->createElement('granularity', 'YYYY-MM-DDThh:mm:ssZ');
|
||||||
|
$identify->appendChild($granularity);
|
||||||
|
|
||||||
|
// TODO: Add support for content compression
|
||||||
|
// $compression = $document->createElement('compression', '...');
|
||||||
|
// $identify->appendChild($compression);
|
||||||
|
|
||||||
|
$this->preparedResponse = $document;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use OCC\OaiPmh2\Database\Record;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "ListIdentifiers" request.
|
||||||
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListIdentifiers
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class ListIdentifiers extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare the response body for verb "ListIdentifiers" and "ListRecords".
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$counter = 0;
|
||||||
|
$completeListSize = 0;
|
||||||
|
$maxRecords = Configuration::getInstance()->maxRecords;
|
||||||
|
|
||||||
|
$params = $request->getAttributes();
|
||||||
|
$verb = $params['verb'];
|
||||||
|
$metadataPrefix = $params['metadataPrefix'] ?? '';
|
||||||
|
$from = $params['from'] ?? null;
|
||||||
|
$until = $params['until'] ?? null;
|
||||||
|
$set = $params['set'] ?? null;
|
||||||
|
$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();
|
||||||
|
if (!in_array($metadataPrefix, array_keys($prefixes->getQueryResult()), true)) {
|
||||||
|
ErrorHandler::getInstance()->withError('cannotDisseminateFormat');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isset($set)) {
|
||||||
|
$sets = Database::getInstance()->getSets();
|
||||||
|
if (!in_array($set, array_keys($sets->getQueryResult()), true)) {
|
||||||
|
ErrorHandler::getInstance()->withError('noSetHierarchy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = Database::getInstance()->getRecords($verb, $metadataPrefix, $counter, $from, $until, $set);
|
||||||
|
if (count($records) === 0) {
|
||||||
|
ErrorHandler::getInstance()->withError('noRecordsMatch');
|
||||||
|
return;
|
||||||
|
} elseif ($records->getResumptionToken() !== null) {
|
||||||
|
$newToken = $records->getResumptionToken();
|
||||||
|
$completeListSize = $newToken->getParameters()['completeListSize'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new Document($request);
|
||||||
|
$list = $document->createElement($verb, '', true);
|
||||||
|
|
||||||
|
/** @var Record $oaiRecord */
|
||||||
|
foreach ($records as $oaiRecord) {
|
||||||
|
if ($verb === 'ListIdentifiers') {
|
||||||
|
$baseNode = $list;
|
||||||
|
} else {
|
||||||
|
$record = $document->createElement('record');
|
||||||
|
$list->appendChild($record);
|
||||||
|
$baseNode = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = $document->createElement('header');
|
||||||
|
if ($oaiRecord->getContent() === '') {
|
||||||
|
$header->setAttribute('status', 'deleted');
|
||||||
|
}
|
||||||
|
$baseNode->appendChild($header);
|
||||||
|
|
||||||
|
$identifier = $document->createElement('identifier', $oaiRecord->getIdentifier());
|
||||||
|
$header->appendChild($identifier);
|
||||||
|
|
||||||
|
$datestamp = $document->createElement('datestamp', $oaiRecord->getLastChanged()->format('Y-m-d\TH:i:s\Z'));
|
||||||
|
$header->appendChild($datestamp);
|
||||||
|
|
||||||
|
foreach ($oaiRecord->getSets() as $oaiSet) {
|
||||||
|
$setSpec = $document->createElement('setSpec', $oaiSet->getName());
|
||||||
|
$header->appendChild($setSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($verb === 'ListRecords' && $oaiRecord->getContent() !== '') {
|
||||||
|
$metadata = $document->createElement('metadata');
|
||||||
|
$baseNode->appendChild($metadata);
|
||||||
|
|
||||||
|
$data = $document->importData($oaiRecord->getContent());
|
||||||
|
$metadata->appendChild($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($oldToken) || isset($newToken)) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use OCC\OaiPmh2\Database\Format;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "ListMetadataFormats" request.
|
||||||
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListMetadataFormats
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class ListMetadataFormats extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare the response body for verb "ListMetadataFormats".
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
/** @var ?string */
|
||||||
|
$identifier = $request->getAttribute('identifier');
|
||||||
|
$formats = Database::getInstance()->getMetadataFormats($identifier);
|
||||||
|
|
||||||
|
if (count($formats) === 0) {
|
||||||
|
if (!isset($identifier) || Database::getInstance()->idDoesExist($identifier)) {
|
||||||
|
ErrorHandler::getInstance()->withError('noMetadataFormats');
|
||||||
|
} else {
|
||||||
|
ErrorHandler::getInstance()->withError('idDoesNotExist');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new Document($request);
|
||||||
|
$listMetadataFormats = $document->createElement('ListMetadataFormats', '', true);
|
||||||
|
|
||||||
|
/** @var Format $oaiFormat */
|
||||||
|
foreach ($formats as $oaiFormat) {
|
||||||
|
$metadataFormat = $document->createElement('metadataFormat');
|
||||||
|
$listMetadataFormats->appendChild($metadataFormat);
|
||||||
|
|
||||||
|
$metadataPrefix = $document->createElement('metadataPrefix', $oaiFormat->getPrefix());
|
||||||
|
$metadataFormat->appendChild($metadataPrefix);
|
||||||
|
|
||||||
|
$schema = $document->createElement('schema', $oaiFormat->getSchema());
|
||||||
|
$metadataFormat->appendChild($schema);
|
||||||
|
|
||||||
|
$metadataNamespace = $document->createElement('metadataNamespace', $oaiFormat->getNamespace());
|
||||||
|
$metadataFormat->appendChild($metadataNamespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->preparedResponse = $document;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "ListRecords" request.
|
||||||
|
* @see https://www.openarchives.org/OAI/openarchivesprotocol.html#ListRecords
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*/
|
||||||
|
class ListRecords extends ListIdentifiers
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* "ListIdentifiers" and "ListRecords" are practically identical except the
|
||||||
|
* former returns the header information only while the latter also returns
|
||||||
|
* the records' data.
|
||||||
|
*/
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAI-PMH 2.0 Data Provider
|
||||||
|
* Copyright (C) 2023 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\Middleware;
|
||||||
|
|
||||||
|
use OCC\OaiPmh2\Configuration;
|
||||||
|
use OCC\OaiPmh2\Database;
|
||||||
|
use OCC\OaiPmh2\Database\Set;
|
||||||
|
use OCC\OaiPmh2\Database\Token;
|
||||||
|
use OCC\OaiPmh2\Document;
|
||||||
|
use OCC\OaiPmh2\Middleware;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the "ListSets" request.
|
||||||
|
* @see https://openarchives.org/OAI/openarchivesprotocol.html#ListSets
|
||||||
|
*
|
||||||
|
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
|
||||||
|
* @package opencultureconsulting/oai-pmh2
|
||||||
|
*
|
||||||
|
* @template Sets of array<string, Set>
|
||||||
|
*/
|
||||||
|
class ListSets extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Prepare the response body for verb "ListSets".
|
||||||
|
*
|
||||||
|
* @param ServerRequestInterface $request The incoming request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareResponse(ServerRequestInterface $request): void
|
||||||
|
{
|
||||||
|
$counter = 0;
|
||||||
|
$completeListSize = 0;
|
||||||
|
$maxRecords = Configuration::getInstance()->maxRecords;
|
||||||
|
|
||||||
|
/** @var ?string */
|
||||||
|
$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);
|
||||||
|
if (count($sets) === 0) {
|
||||||
|
ErrorHandler::getInstance()->withError('noSetHierarchy');
|
||||||
|
return;
|
||||||
|
} elseif ($sets->getResumptionToken() !== null) {
|
||||||
|
$newToken = $sets->getResumptionToken();
|
||||||
|
$completeListSize = $newToken->getParameters()['completeListSize'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$document = new Document($request);
|
||||||
|
$list = $document->createElement('ListSets', '', true);
|
||||||
|
|
||||||
|
/** @var Set $oaiSet */
|
||||||
|
foreach ($sets as $oaiSet) {
|
||||||
|
$set = $document->createElement('set');
|
||||||
|
$list->appendChild($set);
|
||||||
|
|
||||||
|
$setSpec = $document->createElement('setSpec', $oaiSet->getSpec());
|
||||||
|
$set->appendChild($setSpec);
|
||||||
|
|
||||||
|
$setName = $document->createElement('setName', $oaiSet->getName());
|
||||||
|
$set->appendChild($setName);
|
||||||
|
|
||||||
|
if ($oaiSet->getDescription() !== '') {
|
||||||
|
$setDescription = $document->createElement('setDescription', $oaiSet->getDescription());
|
||||||
|
$set->appendChild($setDescription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($oldToken) || isset($newToken)) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue