From 3d79ba06508aed613dd7546f381adfff6ea93523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 10 Nov 2016 13:30:42 +0100 Subject: [PATCH] initial commit --- .gitignore | 16 + build.gradle | 71 ++ gradle/ext.gradle | 12 + gradle/publish.gradle | 66 ++ gradle/sonarqube.gradle | 41 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52928 bytes gradle/wrapper/gradle-wrapper.properties | 6 + oai-client/build.gradle | 33 + oai-client/config/checkstyle/checkstyle.xml | 323 +++++++++ .../org/xbib/oai/client/ClientOAIRequest.java | 174 +++++ .../xbib/oai/client/ClientOAIResponse.java | 15 + .../org/xbib/oai/client/DefaultOAIClient.java | 191 +++++ .../java/org/xbib/oai/client/OAIClient.java | 107 +++ .../org/xbib/oai/client/OAIClientFactory.java | 58 ++ .../client/getrecord/GetRecordRequest.java | 14 + .../client/getrecord/GetRecordResponse.java | 23 + .../oai/client/getrecord/package-info.java | 4 + .../oai/client/identify/IdentifyRequest.java | 15 + .../oai/client/identify/IdentifyResponse.java | 134 ++++ .../oai/client/identify/package-info.java | 4 + .../ListIdentifiersRequest.java | 15 + .../ListIdentifiersResponse.java | 23 + .../client/listidentifiers/package-info.java | 4 + .../ListMetadataFormatsRequest.java | 16 + .../ListMetadataFormatsResponse.java | 23 + .../listmetadataformats/package-info.java | 4 + .../listrecords/ListRecordsFilterReader.java | 215 ++++++ .../listrecords/ListRecordsRequest.java | 28 + .../listrecords/ListRecordsResponse.java | 163 +++++ .../oai/client/listrecords/package-info.java | 4 + .../oai/client/listsets/ListSetsRequest.java | 15 + .../oai/client/listsets/ListSetsResponse.java | 23 + .../oai/client/listsets/package-info.java | 4 + .../org/xbib/oai/client/package-info.java | 4 + .../org/xbib/oai/client/ArxivClientTest.java | 124 ++++ .../org/xbib/oai/client/DNBClientTest.java | 115 +++ .../org/xbib/oai/client/DOAJClientTest.java | 142 ++++ .../org/xbib/oai/client/package-info.java | 4 + oai-client/src/test/resources/log4j2.xml | 13 + .../org/xbib/oai/client/DNB.properties | 1 + .../org/xbib/oai/client/DOAJ.properties | 1 + .../org/xbib/oai/client/ZDB.properties | 1 + .../org/xbib/oai/client/doaj-list-records.xml | 63 ++ .../org/xbib/xml/namespace.properties | 41 ++ oai-client/src/test/resources/xsl/oai2.xsl | 659 ++++++++++++++++++ oai-common/build.gradle | 6 + oai-common/config/checkstyle/checkstyle.xml | 323 +++++++++ .../xbib/oai/DefaultOAIResponseListener.java | 13 + .../main/java/org/xbib/oai/OAIConstants.java | 49 ++ .../main/java/org/xbib/oai/OAIRequest.java | 23 + .../main/java/org/xbib/oai/OAIResponse.java | 12 + .../main/java/org/xbib/oai/OAISession.java | 10 + .../oai/exceptions/BadArgumentException.java | 17 + .../BadResumptionTokenException.java | 15 + .../xbib/oai/exceptions/BadVerbException.java | 13 + .../CannotDisseminateFormatException.java | 14 + .../exceptions/IdDoesNotExistException.java | 14 + .../exceptions/NoRecordsMatchException.java | 14 + .../exceptions/NoSetHierarchyException.java | 14 + .../org/xbib/oai/exceptions/OAIException.java | 24 + .../org/xbib/oai/exceptions/package-info.java | 4 + .../main/java/org/xbib/oai/package-info.java | 4 + .../org/xbib/oai/rdf/RdfResourceHandler.java | 59 ++ .../oai/rdf/RdfSimpleMetadataHandler.java | 140 ++++ .../java/org/xbib/oai/rdf/package-info.java | 4 + .../java/org/xbib/oai/util/RecordHeader.java | 42 ++ .../org/xbib/oai/util/ResumptionToken.java | 171 +++++ .../java/org/xbib/oai/util/URIBuilder.java | 230 ++++++ .../java/org/xbib/oai/util/URIFormatter.java | 138 ++++ .../java/org/xbib/oai/util/package-info.java | 4 + .../org/xbib/oai/xml/MetadataHandler.java | 14 + .../xbib/oai/xml/SimpleMetadataHandler.java | 22 + .../oai/xml/XmlSimpleMetadataHandler.java | 297 ++++++++ .../java/org/xbib/oai/xml/package-info.java | 4 + oai-server/build.gradle | 4 + oai-server/config/checkstyle/checkstyle.xml | 323 +++++++++ .../java/org/xbib/oai/server/OAIServer.java | 120 ++++ .../xbib/oai/server/OAIServiceFactory.java | 58 ++ .../xbib/oai/server/PropertiesOAIServer.java | 144 ++++ .../org/xbib/oai/server/ServerOAIRequest.java | 108 +++ .../xbib/oai/server/ServerOAIResponse.java | 36 + .../getrecord/GetRecordServerRequest.java | 10 + .../getrecord/GetRecordServerResponse.java | 9 + .../oai/server/getrecord/package-info.java | 4 + .../identify/IdentifyServerRequest.java | 9 + .../identify/IdentifyServerResponse.java | 101 +++ .../oai/server/identify/package-info.java | 4 + .../ListIdentifiersServerRequest.java | 10 + .../ListIdentifiersServerResponse.java | 10 + .../server/listidentifiers/package-info.java | 4 + .../ListMetadataFormatsServerRequest.java | 10 + .../ListMetadataFormatsServerResponse.java | 10 + .../listmetadataformats/package-info.java | 4 + .../listrecords/ListRecordsServerRequest.java | 10 + .../ListRecordsServerResponse.java | 58 ++ .../oai/server/listrecords/package-info.java | 4 + .../listsets/ListSetsServerRequest.java | 10 + .../listsets/ListSetsServerResponse.java | 9 + .../oai/server/listsets/package-info.java | 4 + .../org/xbib/oai/server/package-info.java | 4 + .../xbib/oai/server/verb/AbstractVerb.java | 112 +++ .../org/xbib/oai/server/verb/Identify.java | 31 + .../oai/server/verb/ListMetadataFormats.java | 31 + .../xbib/oai/server/verb/package-info.java | 4 + .../org/xbib/oai/server/SimpleServer.java | 116 +++ .../xbib/oai/server/SimpleServiceTest.java | 27 + .../org/xbib/oai/server/package-info.java | 4 + .../services/org.xbib.oai.server.OAIServer | 1 + oai-server/src/test/resources/log4j2.xml | 13 + .../org/xbib/oai/server/test.properties | 7 + settings.gradle | 4 + 111 files changed, 6133 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/ext.gradle create mode 100644 gradle/publish.gradle create mode 100644 gradle/sonarqube.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 oai-client/build.gradle create mode 100644 oai-client/config/checkstyle/checkstyle.xml create mode 100644 oai-client/src/main/java/org/xbib/oai/client/ClientOAIRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/ClientOAIResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/DefaultOAIClient.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/OAIClient.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/OAIClientFactory.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/getrecord/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/identify/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listidentifiers/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsFilterReader.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listrecords/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsRequest.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsResponse.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/listsets/package-info.java create mode 100644 oai-client/src/main/java/org/xbib/oai/client/package-info.java create mode 100644 oai-client/src/test/java/org/xbib/oai/client/ArxivClientTest.java create mode 100644 oai-client/src/test/java/org/xbib/oai/client/DNBClientTest.java create mode 100644 oai-client/src/test/java/org/xbib/oai/client/DOAJClientTest.java create mode 100644 oai-client/src/test/java/org/xbib/oai/client/package-info.java create mode 100644 oai-client/src/test/resources/log4j2.xml create mode 100644 oai-client/src/test/resources/org/xbib/oai/client/DNB.properties create mode 100644 oai-client/src/test/resources/org/xbib/oai/client/DOAJ.properties create mode 100644 oai-client/src/test/resources/org/xbib/oai/client/ZDB.properties create mode 100644 oai-client/src/test/resources/org/xbib/oai/client/doaj-list-records.xml create mode 100644 oai-client/src/test/resources/org/xbib/xml/namespace.properties create mode 100644 oai-client/src/test/resources/xsl/oai2.xsl create mode 100644 oai-common/build.gradle create mode 100644 oai-common/config/checkstyle/checkstyle.xml create mode 100644 oai-common/src/main/java/org/xbib/oai/DefaultOAIResponseListener.java create mode 100644 oai-common/src/main/java/org/xbib/oai/OAIConstants.java create mode 100644 oai-common/src/main/java/org/xbib/oai/OAIRequest.java create mode 100644 oai-common/src/main/java/org/xbib/oai/OAIResponse.java create mode 100644 oai-common/src/main/java/org/xbib/oai/OAISession.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/BadArgumentException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/BadResumptionTokenException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/BadVerbException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/CannotDisseminateFormatException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/IdDoesNotExistException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/NoRecordsMatchException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/NoSetHierarchyException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/OAIException.java create mode 100644 oai-common/src/main/java/org/xbib/oai/exceptions/package-info.java create mode 100644 oai-common/src/main/java/org/xbib/oai/package-info.java create mode 100644 oai-common/src/main/java/org/xbib/oai/rdf/RdfResourceHandler.java create mode 100644 oai-common/src/main/java/org/xbib/oai/rdf/RdfSimpleMetadataHandler.java create mode 100644 oai-common/src/main/java/org/xbib/oai/rdf/package-info.java create mode 100644 oai-common/src/main/java/org/xbib/oai/util/RecordHeader.java create mode 100644 oai-common/src/main/java/org/xbib/oai/util/ResumptionToken.java create mode 100644 oai-common/src/main/java/org/xbib/oai/util/URIBuilder.java create mode 100644 oai-common/src/main/java/org/xbib/oai/util/URIFormatter.java create mode 100644 oai-common/src/main/java/org/xbib/oai/util/package-info.java create mode 100644 oai-common/src/main/java/org/xbib/oai/xml/MetadataHandler.java create mode 100644 oai-common/src/main/java/org/xbib/oai/xml/SimpleMetadataHandler.java create mode 100644 oai-common/src/main/java/org/xbib/oai/xml/XmlSimpleMetadataHandler.java create mode 100644 oai-common/src/main/java/org/xbib/oai/xml/package-info.java create mode 100644 oai-server/build.gradle create mode 100644 oai-server/config/checkstyle/checkstyle.xml create mode 100644 oai-server/src/main/java/org/xbib/oai/server/OAIServer.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/OAIServiceFactory.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/PropertiesOAIServer.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/ServerOAIRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/ServerOAIResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/getrecord/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/identify/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listidentifiers/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listrecords/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerRequest.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerResponse.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/listsets/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/package-info.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/verb/AbstractVerb.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/verb/Identify.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/verb/ListMetadataFormats.java create mode 100644 oai-server/src/main/java/org/xbib/oai/server/verb/package-info.java create mode 100644 oai-server/src/test/java/org/xbib/oai/server/SimpleServer.java create mode 100644 oai-server/src/test/java/org/xbib/oai/server/SimpleServiceTest.java create mode 100644 oai-server/src/test/java/org/xbib/oai/server/package-info.java create mode 100644 oai-server/src/test/resources/META-INF/services/org.xbib.oai.server.OAIServer create mode 100644 oai-server/src/test/resources/log4j2.xml create mode 100644 oai-server/src/test/resources/org/xbib/oai/server/test.properties create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86023d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/data +/work +/logs +/.idea +/target +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +build +/plugins +/sessions +*~ +*.MARC diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ea684cf --- /dev/null +++ b/build.gradle @@ -0,0 +1,71 @@ +plugins { + id "org.sonarqube" version "2.2" + id "org.ajoberstar.github-pages" version "1.6.0-rc.1" + id "org.xbib.gradle.plugin.jbake" version "1.1.0" +} + +allprojects { + + group = 'org.xbib' + version = '1.0.0' + + apply plugin: 'java' + apply plugin: 'maven' + apply plugin: 'signing' + apply plugin: 'findbugs' + apply plugin: 'pmd' + apply plugin: 'checkstyle' + apply plugin: "jacoco" + + repositories { + mavenCentral() + } + + configurations { + wagon + } + + dependencies { + testCompile 'junit:junit:4.12' + testCompile 'org.apache.logging.log4j:log4j-core:2.7' + testCompile 'org.apache.logging.log4j:log4j-jul:2.7' + wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' + } + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-profile" << "compact2" + } + + test { + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } + systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager' + } + + task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource + } + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' + } + artifacts { + archives sourcesJar, javadocJar + } + if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } + } + + apply from: "${rootProject.projectDir}/gradle/ext.gradle" + apply from: "${rootProject.projectDir}/gradle/publish.gradle" + apply from: "${rootProject.projectDir}/gradle/sonarqube.gradle" + +} diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..e541663 --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,12 @@ +ext { + user = 'xbib' + projectName = 'oai' + projectDescription = 'Open Archive Initiative library for Java' + scmUrl = 'https://github.com/xbib/oai' + scmConnection = 'scm:git:git://github.com/xbib/oai.git' + scmDeveloperConnection = 'scm:git:git://github.com/xbib/oai.git' + versions = [ + 'tcnative': '1.1.33.Fork23', + 'alpnboot': '8.1.9.v20160720' + ] +} diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 0000000..caf0531 --- /dev/null +++ b/gradle/publish.gradle @@ -0,0 +1,66 @@ + +task xbibUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('xbibUsername')) { + mavenDeployer { + configuration = configurations.wagon + repository(url: uri('scpexe://xbib.org/repository')) { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + } + } + } + } +} + +task sonatypeUpload(type: Upload, dependsOn: build) { + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: uri(ossrhSnapshotUrl)) { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + groupId project.group + artifactId project.name + version project.version + name project.name + description projectDescription + packaging 'jar' + inceptionYear '2016' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + } +} diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle new file mode 100644 index 0000000..6d4c3fa --- /dev/null +++ b/gradle/sonarqube.gradle @@ -0,0 +1,41 @@ +tasks.withType(FindBugs) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = false + } +} +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} + +jacocoTestReport { + reports { + xml.enabled true + csv.enabled false + xml.destination "${buildDir}/reports/jacoco-xml" + html.destination "${buildDir}/reports/jacoco-html" + } +} + +sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.junit.reportsPath", "build/test-results/test/" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..6ffa237849ef3607e39c3b334a92a65367962071 GIT binary patch literal 52928 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oai-client/src/main/java/org/xbib/oai/client/ClientOAIRequest.java b/oai-client/src/main/java/org/xbib/oai/client/ClientOAIRequest.java new file mode 100644 index 0000000..a2aa038 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/ClientOAIRequest.java @@ -0,0 +1,174 @@ +package org.xbib.oai.client; + +import org.xbib.oai.OAIConstants; +import org.xbib.oai.OAIRequest; +import org.xbib.oai.util.ResumptionToken; +import org.xbib.oai.util.URIBuilder; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +/** + * Client OAI request + */ +public class ClientOAIRequest implements OAIRequest { + + private URIBuilder uriBuilder; + + private DateTimeFormatter dateTimeFormatter; + + private ResumptionToken token; + + private String set; + + private String metadataPrefix; + + private Instant from; + + private Instant until; + + private boolean retry; + + protected ClientOAIRequest() { + uriBuilder = new URIBuilder(); + } + + public void setURL(URL url) { + try { + URI uri = url.toURI(); + uriBuilder.scheme(uri.getScheme()) + .authority(uri.getAuthority()) + .path(uri.getPath()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("invalid URI " + url); + } + } + + public URL getURL() throws MalformedURLException { + return uriBuilder.build().toURL(); + } + + public String getPath() { + return uriBuilder.buildGetPath(); + } + + public void addParameter(String name, String value) { + if (value != null && !value.isEmpty()) { + uriBuilder.addParameter(name, value); + } + } + + @Override + public void setSet(String set) { + this.set = set; + addParameter(OAIConstants.SET_PARAMETER, set); + } + + public String getSet() { + return set; + } + + @Override + public void setMetadataPrefix(String prefix) { + this.metadataPrefix = prefix; + addParameter(OAIConstants.METADATA_PREFIX_PARAMETER, prefix); + } + + public String getMetadataPrefix() { + return metadataPrefix; + } + + public void setDateTimeFormatter(DateTimeFormatter dateTimeFormatter) { + this.dateTimeFormatter = dateTimeFormatter; + } + + @Override + public void setFrom(Instant from) { + this.from = from; + String fromStr = dateTimeFormatter == null ? from.toString() : dateTimeFormatter.format(from); + addParameter(OAIConstants.FROM_PARAMETER, fromStr); + } + + public Instant getFrom() { + return from; + } + + @Override + public void setUntil(Instant until) { + this.until = until; + String untilStr = dateTimeFormatter == null ? until.toString() : dateTimeFormatter.format(until); + addParameter(OAIConstants.UNTIL_PARAMETER, untilStr); + } + + public Instant getUntil() { + return until; + } + + public void setResumptionToken(ResumptionToken token) { + this.token = token; + if (token != null && token.toString() != null) { + // resumption token may have characters that are illegal in URIs like '|' + //String tokenStr = URIFormatter.encode(token.toString(), StandardCharsets.UTF_8); + addParameter(OAIConstants.RESUMPTION_TOKEN_PARAMETER, token.toString()); + } + } + + public ResumptionToken getResumptionToken() { + return token; + } + + public void setRetry(boolean retry) { + this.retry = retry; + } + + public boolean isRetry() { + return retry; + } + + class GetRecord extends ClientOAIRequest { + + public GetRecord() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.GET_RECORD); + } + } + + class Identify extends ClientOAIRequest { + + public Identify() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.IDENTIFY); + } + } + + class ListIdentifiers extends ClientOAIRequest { + + public ListIdentifiers() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.LIST_IDENTIFIERS); + } + } + + class ListMetadataFormats extends ClientOAIRequest { + + public ListMetadataFormats() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.LIST_METADATA_FORMATS); + } + } + + class ListRecordsRequest extends ClientOAIRequest { + + public ListRecordsRequest() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.LIST_RECORDS); + } + + } + + class ListSetsRequest extends ClientOAIRequest { + + public ListSetsRequest() { + addParameter(OAIConstants.VERB_PARAMETER, OAIConstants.LIST_SETS); + } + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/ClientOAIResponse.java b/oai-client/src/main/java/org/xbib/oai/client/ClientOAIResponse.java new file mode 100644 index 0000000..de4cdef --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/ClientOAIResponse.java @@ -0,0 +1,15 @@ +package org.xbib.oai.client; + +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.OAIResponse; + +import java.io.IOException; +import java.io.Writer; + +/** + * Default OAI response + */ +public interface ClientOAIResponse extends OAIResponse { + + void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException; +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/DefaultOAIClient.java b/oai-client/src/main/java/org/xbib/oai/client/DefaultOAIClient.java new file mode 100644 index 0000000..601ebf1 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/DefaultOAIClient.java @@ -0,0 +1,191 @@ +package org.xbib.oai.client; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; + +import org.xbib.helianthus.client.ClientBuilder; +import org.xbib.helianthus.client.ClientFactory; +import org.xbib.helianthus.client.http.HttpClient; +import org.xbib.oai.client.getrecord.GetRecordRequest; +import org.xbib.oai.client.identify.IdentifyRequest; +import org.xbib.oai.client.listidentifiers.ListIdentifiersRequest; +import org.xbib.oai.client.listmetadataformats.ListMetadataFormatsRequest; +import org.xbib.oai.client.listrecords.ListRecordsRequest; +import org.xbib.oai.client.listsets.ListSetsRequest; +import org.xbib.oai.util.ResumptionToken; + +/** + * Default OAI client + */ +public class DefaultOAIClient implements OAIClient { + + private HttpClient client; + + private ClientFactory clientFactory; + + private URL url; + + @Override + public DefaultOAIClient setURL(URL url) throws URISyntaxException { + return setURL(url, false); + } + + @Override + public DefaultOAIClient setURL(URL url, boolean trustAlways) throws URISyntaxException { + this.url = url; + this.clientFactory = ClientFactory.DEFAULT; + this.client = new ClientBuilder("none+" + url.toURI()) + .factory(clientFactory) + .defaultResponseTimeout(Duration.ofMinutes(1L)) // maybe not enough for extreme slow archive servers... + .build(HttpClient.class); + return this; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public HttpClient getHttpClient() { + return client; + } + + @Override + public ClientFactory getFactory() { + return clientFactory; + } + + @Override + public IdentifyRequest newIdentifyRequest() { + IdentifyRequest request = new IdentifyRequest(); + request.setURL(url); + return request; + } + + @Override + public ListMetadataFormatsRequest newListMetadataFormatsRequest() { + ListMetadataFormatsRequest request = new ListMetadataFormatsRequest(); + request.setURL(getURL()); + return request; + } + + @Override + public ListSetsRequest newListSetsRequest() { + ListSetsRequest request = new ListSetsRequest(); + request.setURL(getURL()); + return request; + } + + @Override + public ListIdentifiersRequest newListIdentifiersRequest() { + ListIdentifiersRequest request = new ListIdentifiersRequest(); + request.setURL(getURL()); + return request; + } + + @Override + public GetRecordRequest newGetRecordRequest() { + GetRecordRequest request = new GetRecordRequest(); + request.setURL(getURL()); + return request; + } + + @Override + public ListRecordsRequest newListRecordsRequest() { + ListRecordsRequest request = new ListRecordsRequest(); + request.setURL(getURL()); + return request; + } + + @Override + public IdentifyRequest resume(IdentifyRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newIdentifyRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public ListRecordsRequest resume(ListRecordsRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newListRecordsRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public ListIdentifiersRequest resume(ListIdentifiersRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newListIdentifiersRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public ListMetadataFormatsRequest resume(ListMetadataFormatsRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newListMetadataFormatsRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public ListSetsRequest resume(ListSetsRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newListSetsRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public GetRecordRequest resume(GetRecordRequest request, ResumptionToken token) { + if (request.isRetry()) { + request.setRetry(false); + return request; + } + if (token == null) { + return null; + } + request = newGetRecordRequest(); + request.setResumptionToken(token); + return request; + } + + @Override + public void close() throws IOException { + + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/OAIClient.java b/oai-client/src/main/java/org/xbib/oai/client/OAIClient.java new file mode 100644 index 0000000..2f90463 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/OAIClient.java @@ -0,0 +1,107 @@ +package org.xbib.oai.client; + +import java.net.URISyntaxException; +import java.net.URL; + +import org.xbib.helianthus.client.ClientFactory; +import org.xbib.helianthus.client.http.HttpClient; +import org.xbib.oai.OAIConstants; +import org.xbib.oai.OAISession; +import org.xbib.oai.client.getrecord.GetRecordRequest; +import org.xbib.oai.client.identify.IdentifyRequest; +import org.xbib.oai.client.listidentifiers.ListIdentifiersRequest; +import org.xbib.oai.client.listmetadataformats.ListMetadataFormatsRequest; +import org.xbib.oai.client.listrecords.ListRecordsRequest; +import org.xbib.oai.client.listsets.ListSetsRequest; +import org.xbib.oai.util.ResumptionToken; + +/** + * OAI client API + * + */ +public interface OAIClient extends OAISession, OAIConstants { + + OAIClient setURL(URL uri, boolean trustAlways) throws URISyntaxException; + + OAIClient setURL(URL uri) throws URISyntaxException; + + URL getURL(); + + HttpClient getHttpClient(); + + ClientFactory getFactory(); + + /** + * This verb is used to retrieve information about a repository. + * Some of the information returned is required as part of the OAI-PMH. + * Repositories may also employ the Identify verb to return additional + * descriptive information. + * @return identify request + */ + IdentifyRequest newIdentifyRequest(); + + IdentifyRequest resume(IdentifyRequest request, ResumptionToken token); + + /** + * This verb is an abbreviated form of ListRecords, retrieving only + * headers rather than records. Optional arguments permit selective + * harvesting of headers based on set membership and/or datestamp. + * Depending on the repository's support for deletions, a returned + * header may have a status attribute of "deleted" if a record + * matching the arguments specified in the request has been deleted. + * @return list identifiers request + * + */ + ListIdentifiersRequest newListIdentifiersRequest(); + + ListIdentifiersRequest resume(ListIdentifiersRequest request, ResumptionToken token); + + /** + * This verb is used to retrieve the metadata formats available + * from a repository. An optional argument restricts the request + * to the formats available for a specific item. + * @return list metadata formats request + */ + ListMetadataFormatsRequest newListMetadataFormatsRequest(); + + ListMetadataFormatsRequest resume(ListMetadataFormatsRequest request, ResumptionToken token); + + /** + * This verb is used to retrieve the set structure of a repository, + * useful for selective harvesting. + * @return list sets request + */ + ListSetsRequest newListSetsRequest(); + + ListSetsRequest resume(ListSetsRequest request, ResumptionToken token); + + /** + * This verb is used to harvest records from a repository. + * Optional arguments permit selective harvesting of records based on + * set membership and/or datestamp. Depending on the repository's + * support for deletions, a returned header may have a status + * attribute of "deleted" if a record matching the arguments + * specified in the request has been deleted. No metadata + * will be present for records with deleted status. + * @return list records request + */ + ListRecordsRequest newListRecordsRequest(); + + ListRecordsRequest resume(ListRecordsRequest request, ResumptionToken token); + + /** + * This verb is used to retrieve an individual metadata record from + * a repository. Required arguments specify the identifier of the item + * from which the record is requested and the format of the metadata + * that should be included in the record. Depending on the level at + * which a repository tracks deletions, a header with a "deleted" value + * for the status attribute may be returned, in case the metadata format + * specified by the metadataPrefix is no longer available from the + * repository or from the specified item. + * @return get record request + */ + GetRecordRequest newGetRecordRequest(); + + GetRecordRequest resume(GetRecordRequest request, ResumptionToken token); + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/OAIClientFactory.java b/oai-client/src/main/java/org/xbib/oai/client/OAIClientFactory.java new file mode 100644 index 0000000..3ddaee0 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/OAIClientFactory.java @@ -0,0 +1,58 @@ +package org.xbib.oai.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + + +/** + * Factory for OAI clients + * + */ +public class OAIClientFactory { + + private final static OAIClientFactory instance = new OAIClientFactory(); + + private OAIClientFactory() { + } + + public static OAIClientFactory getInstance() { + return instance; + } + + public static OAIClient newClient() { + return new DefaultOAIClient(); + } + + public static OAIClient newClient(String spec) { + return newClient(spec, false); + } + + public static OAIClient newClient(String spec, boolean trustAll) { + Properties properties = new Properties(); + InputStream in = instance.getClass().getResourceAsStream("/org/xbib/oai/client/" + spec + ".properties"); + if (in != null) { + try { + properties.load(in); + } catch (IOException ex) { + // ignore + } + DefaultOAIClient client = new DefaultOAIClient(); + try { + client.setURL(new URL(properties.getProperty("uri")), trustAll); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + return client; + } else { + DefaultOAIClient client = new DefaultOAIClient(); + try { + client.setURL(new URL(spec)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + return client; + } + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordRequest.java b/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordRequest.java new file mode 100644 index 0000000..a196b85 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordRequest.java @@ -0,0 +1,14 @@ +package org.xbib.oai.client.getrecord; + +import org.xbib.oai.client.ClientOAIRequest; + +/** + * + */ +public class GetRecordRequest extends ClientOAIRequest { + + public GetRecordRequest() { + super(); + addParameter(VERB_PARAMETER, GET_RECORD); + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordResponse.java b/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordResponse.java new file mode 100644 index 0000000..27a977a --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/getrecord/GetRecordResponse.java @@ -0,0 +1,23 @@ +package org.xbib.oai.client.getrecord; + +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; + +import java.io.IOException; +import java.io.Writer; + +/** + * + */ +public class GetRecordResponse implements ClientOAIResponse { + + @Override + public void to(Writer writer) throws IOException { + + } + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/getrecord/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/getrecord/package-info.java new file mode 100644 index 0000000..25df66d --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/getrecord/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI get record verb. + */ +package org.xbib.oai.client.getrecord; diff --git a/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyRequest.java b/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyRequest.java new file mode 100644 index 0000000..6f0d7ae --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyRequest.java @@ -0,0 +1,15 @@ +package org.xbib.oai.client.identify; + +import org.xbib.oai.client.ClientOAIRequest; +import org.xbib.oai.OAIRequest; + +/** + * + */ +public class IdentifyRequest extends ClientOAIRequest implements OAIRequest { + + public IdentifyRequest() { + super(); + addParameter(VERB_PARAMETER, IDENTIFY); + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyResponse.java b/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyResponse.java new file mode 100644 index 0000000..4ffa3a1 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/identify/IdentifyResponse.java @@ -0,0 +1,134 @@ +package org.xbib.oai.client.identify; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * + */ +public class IdentifyResponse implements ClientOAIResponse { + + private String repositoryName; + + private URL baseURL; + + private String protocolVersion; + + private List adminEmails = new ArrayList<>(); + + private Date earliestDatestamp; + + private String deletedRecord; + + private String granularity; + + private String compression; + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + InputSource is = new InputSource(new StringReader(message.content().toStringUtf8())); + Document doc = db.parse(is); + setGranularity(getString("granularity", doc.getDocumentElement())); + } catch (ParserConfigurationException | SAXException e) { + throw new IOException(e); + } + } + + @Override + public void to(Writer writer) throws IOException { + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getRepositoryName() { + return repositoryName; + } + + public void setBaseURL(URL url) { + this.baseURL = url; + } + + public URL getBaseURL() { + return baseURL; + } + + public void setProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public void addAdminEmail(String email) { + adminEmails.add(email); + } + + public List getAdminEmails() { + return adminEmails; + } + + public void setEarliestDatestamp(Date earliestDatestamp) { + this.earliestDatestamp = earliestDatestamp; + } + + public Date getEarliestDatestamp() { + return earliestDatestamp; + } + + public void setDeletedRecord(String deletedRecord) { + this.deletedRecord = deletedRecord; + } + + public String getDeleteRecord() { + return deletedRecord; + } + + public void setGranularity(String granularity) { + this.granularity = granularity; + } + + public String getGranularity() { + return granularity; + } + + public void setCompression(String compression) { + this.compression = compression; + } + + public String getCompression() { + return compression; + } + + private String getString(String tagName, Element element) { + NodeList list = element.getElementsByTagName(tagName); + if (list != null && list.getLength() > 0) { + NodeList subList = list.item(0).getChildNodes(); + if (subList != null && subList.getLength() > 0) { + return subList.item(0).getNodeValue(); + } + } + return null; + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/identify/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/identify/package-info.java new file mode 100644 index 0000000..d84d84d --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/identify/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI identify verb. + */ +package org.xbib.oai.client.identify; diff --git a/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersRequest.java b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersRequest.java new file mode 100644 index 0000000..28b0a66 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersRequest.java @@ -0,0 +1,15 @@ +package org.xbib.oai.client.listidentifiers; + +import org.xbib.oai.client.ClientOAIRequest; +import org.xbib.oai.OAIRequest; + +/** + * + */ +public class ListIdentifiersRequest extends ClientOAIRequest implements OAIRequest { + + public ListIdentifiersRequest() { + super(); + addParameter(VERB_PARAMETER, LIST_IDENTIFIERS); + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersResponse.java b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersResponse.java new file mode 100644 index 0000000..ec57be0 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/ListIdentifiersResponse.java @@ -0,0 +1,23 @@ +package org.xbib.oai.client.listidentifiers; + +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; + +import java.io.IOException; +import java.io.Writer; + +/** + * + */ +public class ListIdentifiersResponse implements ClientOAIResponse { + + @Override + public void to(Writer writer) throws IOException { + + } + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/package-info.java new file mode 100644 index 0000000..a73e8b2 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listidentifiers/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list identifiers verb. + */ +package org.xbib.oai.client.listidentifiers; diff --git a/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsRequest.java b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsRequest.java new file mode 100644 index 0000000..6007604 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsRequest.java @@ -0,0 +1,16 @@ +package org.xbib.oai.client.listmetadataformats; + +import org.xbib.oai.client.ClientOAIRequest; +import org.xbib.oai.OAIRequest; + +/** + * + */ +public class ListMetadataFormatsRequest extends ClientOAIRequest implements OAIRequest { + + public ListMetadataFormatsRequest() { + super(); + addParameter(VERB_PARAMETER, LIST_METADATA_FORMATS); + } + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsResponse.java b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsResponse.java new file mode 100644 index 0000000..fc1c80d --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/ListMetadataFormatsResponse.java @@ -0,0 +1,23 @@ +package org.xbib.oai.client.listmetadataformats; + +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; + +import java.io.IOException; +import java.io.Writer; + +/** + * + */ +public class ListMetadataFormatsResponse implements ClientOAIResponse { + + @Override + public void to(Writer writer) throws IOException { + + } + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/package-info.java new file mode 100644 index 0000000..7007096 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listmetadataformats/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list metadata formats verb. + */ +package org.xbib.oai.client.listmetadataformats; diff --git a/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsFilterReader.java b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsFilterReader.java new file mode 100644 index 0000000..b41bf1e --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsFilterReader.java @@ -0,0 +1,215 @@ +package org.xbib.oai.client.listrecords; + +import org.xbib.content.xml.util.XMLFilterReader; +import org.xbib.oai.OAIConstants; +import org.xbib.oai.util.RecordHeader; +import org.xbib.oai.util.ResumptionToken; +import org.xbib.oai.xml.MetadataHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + */ +public class ListRecordsFilterReader extends XMLFilterReader { + + private static final Logger logger = Logger.getLogger(ListRecordsFilterReader.class.getName()); + + private final ListRecordsRequest request; + + private final ListRecordsResponse response; + + private StringBuilder content; + + private RecordHeader header; + + private ResumptionToken token; + + private boolean inMetadata; + + ListRecordsFilterReader(ListRecordsRequest request, ListRecordsResponse response) { + super(); + this.request = request; + this.response = response; + this.content = new StringBuilder(); + this.inMetadata = false; + } + + public ResumptionToken getResumptionToken() { + return token; + } + + public ListRecordsResponse getResponse() { + return response; + } + + @Override + public void startDocument() throws SAXException { + logger.log(Level.FINE, "start of document"); + super.startDocument(); + request.setResumptionToken(null); + } + + @Override + public void endDocument() throws SAXException { + logger.log(Level.FINE, "end of document"); + super.endDocument(); + } + + @Override + public void startElement(String uri, String localname, String qname, Attributes atts) throws SAXException { + super.startElement(uri, localname, qname, atts); + if (OAIConstants.NS_URI.equals(uri)) { + switch (localname) { + case "header": + header = new RecordHeader(); + break; + case "error": + response.setError(atts.getValue("code")); + break; + case "metadata": + inMetadata = true; + for (MetadataHandler mh : request.getHandlers()) { + mh.startDocument(); + } + break; + case "resumptionToken": + try { + token = ResumptionToken.newToken(null); + String cursor = atts.getValue("cursor"); + if (cursor != null) { + token.setCursor(Integer.parseInt(cursor)); + } + String completeListSize = atts.getValue("completeListSize"); + if (completeListSize != null) { + token.setCompleteListSize(Integer.parseInt(completeListSize)); + } + if (!token.isComplete()) { + request.setResumptionToken(token); + } + } catch (Exception e) { + throw new SAXException(e); + } + break; + } + return; + } + if (inMetadata) { + for (MetadataHandler mh : request.getHandlers()) { + mh.startElement(uri, localname, qname, atts); + } + } + } + + @Override + public void endElement(String nsURI, String localname, String qname) throws SAXException { + super.endElement(nsURI, localname, qname); + if (OAIConstants.NS_URI.equals(nsURI)) { + switch (localname) { + case "header": + for (MetadataHandler mh : request.getHandlers()) { + mh.setHeader(header); + } + header = new RecordHeader(); + break; + case "metadata": + for (MetadataHandler mh : request.getHandlers()) { + mh.endDocument(); + } + inMetadata = false; + break; + case "responseDate": + response.setDate(Instant.parse(content.toString().trim())); + break; + case "resumptionToken": + if (token != null && content != null && content.length() > 0) { + token.setValue(content.toString()); + // feedback to request + request.setResumptionToken(token); + } else { + logger.log(Level.WARNING, "empty resumption token value"); + // some servers send a null or an empty token as last token + token = null; + request.setResumptionToken(null); + } + break; + case "identifier": + if (header != null && content != null && content.length() > 0) { + String id = content.toString().trim(); + header.setIdentifier(id); + } + break; + case "datestamp": + if (header != null && content != null && content.length() > 0) { + try { + header.setDate(Instant.parse(content.toString().trim())); + } catch (DateTimeParseException e) { + // not "seconds ISO" + } + try { + LocalDateTime ldt = LocalDateTime.parse(content.toString().trim(), + DateTimeFormatter.ofPattern("yyyy-MM-dd")); + header.setDate(Instant.from(ldt)); + } catch (DateTimeParseException e) { + // not "day ISO" + } + } + break; + case "setSpec": + if (header != null && content != null && content.length() > 0) { + header.setSetspec(content.toString().trim()); + } + break; + } + if (content != null) { + content.setLength(0); + } + return; + } + if (inMetadata) { + for (MetadataHandler mh : request.getHandlers()) { + mh.endElement(nsURI, localname, qname); + } + } + content.setLength(0); + } + + @Override + public void characters(char[] chars, int start, int length) throws SAXException { + super.characters(chars, start, length); + content.append(new String(chars, start, length).trim()); + if (inMetadata) { + for (MetadataHandler mh : request.getHandlers()) { + mh.characters(chars, start, length); + } + } + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + super.startPrefixMapping(prefix, uri); + if (inMetadata) { + for (MetadataHandler mh : request.getHandlers()) { + mh.startPrefixMapping(prefix, uri); + } + } + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + super.endPrefixMapping(prefix); + if (inMetadata) { + for (MetadataHandler mh : request.getHandlers()) { + mh.endPrefixMapping(prefix); + } + } + } + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsRequest.java b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsRequest.java new file mode 100644 index 0000000..9069cc8 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsRequest.java @@ -0,0 +1,28 @@ +package org.xbib.oai.client.listrecords; + +import org.xbib.oai.client.ClientOAIRequest; +import org.xbib.oai.OAIConstants; +import org.xbib.oai.xml.MetadataHandler; + +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +public class ListRecordsRequest extends ClientOAIRequest { + + private List handlers = new LinkedList<>(); + + public ListRecordsRequest() { + super(); + addParameter(OAIConstants.VERB_PARAMETER, LIST_RECORDS); + } + public ListRecordsRequest addHandler(MetadataHandler handler) { + handlers.add(handler); + return this; + } + + public List getHandlers() { return handlers; } + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsResponse.java b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsResponse.java new file mode 100644 index 0000000..8f9099d --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listrecords/ListRecordsResponse.java @@ -0,0 +1,163 @@ +package org.xbib.oai.client.listrecords; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.util.AsciiString; +import org.xbib.content.xml.transform.TransformerURIResolver; +import org.xbib.content.xml.util.XMLUtil; +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; +import org.xbib.oai.exceptions.BadArgumentException; +import org.xbib.oai.exceptions.BadResumptionTokenException; +import org.xbib.oai.exceptions.NoRecordsMatchException; +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.util.ResumptionToken; +import org.xml.sax.InputSource; + +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringReader; +import java.io.Writer; +import java.text.MessageFormat; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + */ +public class ListRecordsResponse implements ClientOAIResponse { + + private static final Logger logger = Logger.getLogger(ListRecordsResponse.class.getName()); + private static final String[] RETRY_AFTER_HEADERS = { + "retry-after", "Retry-after", "Retry-After" + }; + + private final ListRecordsRequest request; + + private ListRecordsFilterReader filterreader; + + private long retryAfterMillis; + + private String error; + + private Instant date; + + public ListRecordsResponse(ListRecordsRequest request) { + this.request = request; + this.retryAfterMillis = 20 * 1000; // 20 seconds by default + } + + public ListRecordsResponse setRetryAfter(long millis) { + this.retryAfterMillis = millis; + return this; + } + + public void setError(String error) { + this.error = error; + } + + public String getError() { + return error; + } + + public void setDate(Instant date) { + this.date = date; + } + + public Instant getDate() { + return date; + } + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + String content = message.content().toStringUtf8(); + int status = message.status().code(); + if (status == 503) { + long secs = retryAfterMillis / 1000; + if (message.headers() != null) { + for (String retryAfterHeader : RETRY_AFTER_HEADERS) { + String retryAfter = message.headers().get(AsciiString.of(retryAfterHeader)); + if (retryAfter == null) { + continue; + } + secs = Long.parseLong(retryAfter); + if (!isDigits(retryAfter)) { + // parse RFC date, e.g. Fri, 31 Dec 1999 23:59:59 GMT + Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(retryAfter)); + secs = ChronoUnit.SECONDS.between(instant, Instant.now()); + logger.log(Level.INFO, MessageFormat.format("parsed delay seconds is {0}", secs)); + } + logger.log(Level.INFO, MessageFormat.format("setting delay seconds to {0}", secs)); + } + } + request.setRetry(true); + try { + if (secs > 0L) { + logger.log(Level.INFO, MessageFormat.format("waiting for {0} seconds (retry-after)", secs)); + Thread.sleep(1000 * secs); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.log(Level.SEVERE, "interrupted"); + } + return; + } + if (status != 200) { + throw new IOException("status = " + status + " response = " + content); + } + // activate XSLT only if OAI XML content type is returned + String contentType = message.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null && !contentType.startsWith("text/xml")) { + throw new IOException("no XML content type in response: " + contentType); + } + this.filterreader = new ListRecordsFilterReader(request, this); + try { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setURIResolver(new TransformerURIResolver("xsl")); + Transformer transformer = transformerFactory.newTransformer(); + Source source = new SAXSource(filterreader, new InputSource(new StringReader(XMLUtil.sanitize(content)))); + StreamResult streamResult = new StreamResult(writer); + logger.log(Level.FINE, "transforming"); + transformer.transform(source, streamResult); + if ("noRecordsMatch".equals(error)) { + throw new NoRecordsMatchException("metadataPrefix=" + request.getMetadataPrefix() + + ",set=" + request.getSet() + + ",from=" + request.getFrom() + + ",until=" + request.getUntil()); + } else if ("badResumptionToken".equals(error)) { + throw new BadResumptionTokenException(request.getResumptionToken()); + } else if ("badArgument".equals(error)) { + throw new BadArgumentException(); + } else if (error != null) { + throw new OAIException(error); + } + } catch (TransformerException t) { + throw new IOException(t); + } + } + + @Override + public void to(Writer writer) throws IOException { + } + + private boolean isDigits(String str) { + for (int i = 0; i < str.length(); i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } + + public ResumptionToken getResumptionToken() { + return filterreader != null ? filterreader.getResumptionToken() : null; + } + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listrecords/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/listrecords/package-info.java new file mode 100644 index 0000000..eb9ad16 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listrecords/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list records verb. + */ +package org.xbib.oai.client.listrecords; diff --git a/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsRequest.java b/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsRequest.java new file mode 100644 index 0000000..e1f44fa --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsRequest.java @@ -0,0 +1,15 @@ +package org.xbib.oai.client.listsets; + +import org.xbib.oai.client.ClientOAIRequest; + +/** + * + */ +public class ListSetsRequest extends ClientOAIRequest { + + public ListSetsRequest() { + super(); + addParameter(VERB_PARAMETER, LIST_SETS); + } + +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsResponse.java b/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsResponse.java new file mode 100644 index 0000000..6253100 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listsets/ListSetsResponse.java @@ -0,0 +1,23 @@ +package org.xbib.oai.client.listsets; + +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.oai.client.ClientOAIResponse; + +import java.io.IOException; +import java.io.Writer; + +/** + * + */ +public class ListSetsResponse implements ClientOAIResponse { + + @Override + public void to(Writer writer) throws IOException { + + } + + @Override + public void receivedResponse(AggregatedHttpMessage message, Writer writer) throws IOException { + + } +} diff --git a/oai-client/src/main/java/org/xbib/oai/client/listsets/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/listsets/package-info.java new file mode 100644 index 0000000..56a18c7 --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/listsets/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list sets verb. + */ +package org.xbib.oai.client.listsets; diff --git a/oai-client/src/main/java/org/xbib/oai/client/package-info.java b/oai-client/src/main/java/org/xbib/oai/client/package-info.java new file mode 100644 index 0000000..4f83d4f --- /dev/null +++ b/oai-client/src/main/java/org/xbib/oai/client/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI client. + */ +package org.xbib.oai.client; diff --git a/oai-client/src/test/java/org/xbib/oai/client/ArxivClientTest.java b/oai-client/src/test/java/org/xbib/oai/client/ArxivClientTest.java new file mode 100644 index 0000000..c9557d2 --- /dev/null +++ b/oai-client/src/test/java/org/xbib/oai/client/ArxivClientTest.java @@ -0,0 +1,124 @@ +package org.xbib.oai.client; + +import static org.junit.Assert.assertTrue; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Test; +import org.xbib.helianthus.client.http.HttpClient; +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.helianthus.common.http.HttpHeaderNames; +import org.xbib.helianthus.common.http.HttpHeaders; +import org.xbib.helianthus.common.http.HttpMethod; +import org.xbib.oai.client.identify.IdentifyRequest; +import org.xbib.oai.client.identify.IdentifyResponse; +import org.xbib.oai.client.listrecords.ListRecordsRequest; +import org.xbib.oai.client.listrecords.ListRecordsResponse; +import org.xbib.oai.xml.SimpleMetadataHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.ConnectException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; + +/** + * + */ +public class ArxivClientTest { + + private static final Logger logger = LogManager.getLogger(ArxivClientTest.class.getName()); + + @Test + public void testListRecordsArxiv() throws Exception { + try { + OAIClient client = OAIClientFactory.newClient("http://export.arxiv.org/oai2"); + IdentifyRequest identifyRequest = client.newIdentifyRequest(); + HttpClient httpClient = client.getHttpClient(); + AggregatedHttpMessage response = httpClient.execute(HttpHeaders.of(HttpMethod.GET, identifyRequest.getPath()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + IdentifyResponse identifyResponse = new IdentifyResponse(); + identifyResponse.receivedResponse(response, new StringWriter()); + String granularity = identifyResponse.getGranularity(); + logger.info("granularity = {}", granularity); + DateTimeFormatter dateTimeFormatter = "YYYY-MM-DD".equals(granularity) ? + DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("GMT")) : null; + // ArXiv wants us to wait 20 secs between *every* HTTP request, so we must wait here + logger.info("waiting 20 seconds"); + Thread.sleep(20 * 1000L); + ListRecordsRequest listRecordsRequest = client.newListRecordsRequest(); + listRecordsRequest.setDateTimeFormatter(dateTimeFormatter); + listRecordsRequest.setFrom(Instant.parse("2016-11-01T00:00:00Z")); + listRecordsRequest.setUntil(Instant.parse("2016-11-02T00:00:00Z")); + listRecordsRequest.setMetadataPrefix("arXiv"); + final AtomicLong count = new AtomicLong(0L); + SimpleMetadataHandler simpleMetadataHandler = new SimpleMetadataHandler() { + @Override + public void startDocument() throws SAXException { + logger.debug("start doc"); + } + + @Override + public void endDocument() throws SAXException { + logger.debug("end doc"); + count.incrementAndGet(); + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + } + + @Override + public void startElement(String ns, String localname, String qname, Attributes atrbts) throws SAXException { + } + + @Override + public void endElement(String ns, String localname, String qname) throws SAXException { + } + + @Override + public void characters(char[] chars, int pos, int len) throws SAXException { + } + + }; + File file = File.createTempFile("arxiv.", ".xml"); + file.deleteOnExit(); + FileWriter fileWriter = new FileWriter(file); + while (listRecordsRequest != null) { + try { + listRecordsRequest.addHandler(simpleMetadataHandler); + ListRecordsResponse listRecordsResponse = new ListRecordsResponse(listRecordsRequest); + logger.info("sending {}", listRecordsRequest.getPath()); + response = httpClient.execute(HttpHeaders.of(HttpMethod.GET, listRecordsRequest.getPath()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + logger.debug("response headers = {} resumption-token = {}", + response.headers(), listRecordsResponse.getResumptionToken()); + listRecordsResponse.receivedResponse(response, fileWriter); + listRecordsRequest = client.resume(listRecordsRequest, listRecordsResponse.getResumptionToken()); + } catch (IOException e) { + logger.error(e.getMessage(), e); + listRecordsRequest = null; + } + } + fileWriter.close(); + client.close(); + logger.info("count={}", count.get()); + assertTrue(count.get() > 0L); + } catch (ConnectException | ExecutionException e) { + logger.warn("skipped, can not connect", e); + } catch (InterruptedException | IOException e) { + throw e; + } + } +} diff --git a/oai-client/src/test/java/org/xbib/oai/client/DNBClientTest.java b/oai-client/src/test/java/org/xbib/oai/client/DNBClientTest.java new file mode 100644 index 0000000..e8f0b6c --- /dev/null +++ b/oai-client/src/test/java/org/xbib/oai/client/DNBClientTest.java @@ -0,0 +1,115 @@ +package org.xbib.oai.client; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.ConnectException; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Test; +import org.xbib.helianthus.client.http.HttpClient; +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.helianthus.common.http.HttpHeaderNames; +import org.xbib.helianthus.common.http.HttpHeaders; +import org.xbib.helianthus.common.http.HttpMethod; +import org.xbib.oai.client.identify.IdentifyRequest; +import org.xbib.oai.client.listrecords.ListRecordsRequest; +import org.xbib.oai.client.listrecords.ListRecordsResponse; +import org.xbib.oai.xml.SimpleMetadataHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class DNBClientTest { + + private static final Logger logger = LogManager.getLogger(DNBClientTest.class.getName()); + + @Test + public void testIdentify() throws Exception { + OAIClient client = OAIClientFactory.newClient("http://services.dnb.de/oai/repository"); + IdentifyRequest request = client.newIdentifyRequest(); + HttpClient httpClient = client.getHttpClient(); + assertEquals("/oai/repository?verb=Identify", request.getPath()); + AggregatedHttpMessage response = httpClient.get(request.getPath()).aggregate().get(); + logger.info("{}", response.content().toStringUtf8()); + } + + @Test + public void testListRecordsDNB() throws Exception { + try { + OAIClient client = OAIClientFactory.newClient("http://services.dnb.de/oai/repository"); + ListRecordsRequest listRecordsRequest = client.newListRecordsRequest(); + listRecordsRequest.setFrom(Instant.parse("2016-01-01T00:00:00Z")); + listRecordsRequest.setUntil(Instant.parse("2016-01-10T00:00:00Z")); + listRecordsRequest.setSet("bib"); + listRecordsRequest.setMetadataPrefix("PicaPlus-xml"); + final AtomicLong count = new AtomicLong(0L); + SimpleMetadataHandler simpleMetadataHandler = new SimpleMetadataHandler() { + @Override + public void startDocument() throws SAXException { + logger.debug("startDocument"); + } + + @Override + public void endDocument() throws SAXException { + count.incrementAndGet(); + logger.debug("endDocument"); + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + } + + @Override + public void startElement(String ns, String localname, String qname, Attributes atrbts) throws SAXException { + } + + @Override + public void endElement(String ns, String localname, String qname) throws SAXException { + } + + @Override + public void characters(char[] chars, int pos, int len) throws SAXException { + } + + }; + File file = File.createTempFile("dnb-bib-pica.", ".xml"); + file.deleteOnExit(); + FileWriter sw = new FileWriter(file); + while (listRecordsRequest != null) { + try { + ListRecordsResponse listRecordsResponse = new ListRecordsResponse(listRecordsRequest); + listRecordsRequest.addHandler(simpleMetadataHandler); + HttpClient httpClient = client.getHttpClient(); + AggregatedHttpMessage response = httpClient.execute(HttpHeaders.of(HttpMethod.GET, listRecordsRequest.getPath()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + String content = response.content().toStringUtf8(); + listRecordsResponse.receivedResponse(response, sw); + listRecordsRequest = client.resume(listRecordsRequest, listRecordsResponse.getResumptionToken()); + } catch (IOException e) { + logger.error(e.getMessage(), e); + listRecordsRequest = null; + } + } + sw.close(); + client.close(); + logger.info("count={}", count.get()); + } catch (ConnectException | ExecutionException e) { + logger.warn("skipped, can not connect"); + } catch (IOException e) { + logger.warn("skipped, HTTP exception"); + } + } +} diff --git a/oai-client/src/test/java/org/xbib/oai/client/DOAJClientTest.java b/oai-client/src/test/java/org/xbib/oai/client/DOAJClientTest.java new file mode 100644 index 0000000..3288676 --- /dev/null +++ b/oai-client/src/test/java/org/xbib/oai/client/DOAJClientTest.java @@ -0,0 +1,142 @@ +package org.xbib.oai.client; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Test; +import org.xbib.helianthus.client.Clients; +import org.xbib.helianthus.client.http.HttpClient; +import org.xbib.helianthus.common.http.AggregatedHttpMessage; +import org.xbib.helianthus.common.http.HttpHeaderNames; +import org.xbib.helianthus.common.http.HttpHeaders; +import org.xbib.helianthus.common.http.HttpMethod; +import org.xbib.oai.client.identify.IdentifyRequest; +import org.xbib.oai.client.identify.IdentifyResponse; +import org.xbib.oai.client.listrecords.ListRecordsRequest; +import org.xbib.oai.client.listrecords.ListRecordsResponse; +import org.xbib.oai.xml.SimpleMetadataHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.net.ConnectException; +import java.net.URI; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertTrue; + +/** + * + */ +public class DOAJClientTest { + + private static final Logger logger = LogManager.getLogger(DOAJClientTest.class.getName()); + + @Test + public void testListRecordsDOAJ() throws InterruptedException, TimeoutException, IOException { + try { + // will redirect to https://doaj.org/oai + OAIClient oaiClient = OAIClientFactory.newClient("http://doaj.org/oai", true); + IdentifyRequest identifyRequest = oaiClient.newIdentifyRequest(); + HttpClient client = oaiClient.getHttpClient(); + AggregatedHttpMessage response = client.execute(HttpHeaders.of(HttpMethod.GET, identifyRequest.getPath()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + // follow a maximum of 10 HTTP redirects + int max = 10; + while (response.followUrl() != null && max-- > 0) { + URI uri = URI.create(response.followUrl()); + client = Clients.newClient(oaiClient.getFactory(), "none+" + uri, HttpClient.class); + response = client.execute(HttpHeaders.of(HttpMethod.GET, response.followUrl()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + } + IdentifyResponse identifyResponse = new IdentifyResponse(); + String content = response.content().toStringUtf8(); + logger.debug("identifyResponse = {}", content); + identifyResponse.receivedResponse(response, new StringWriter()); + String granularity = identifyResponse.getGranularity(); + logger.info("granularity = {}", granularity); + DateTimeFormatter dateTimeFormatter = "YYYY-MM-DD".equals(granularity) ? + DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("GMT")) : null; + ListRecordsRequest listRecordsRequest = oaiClient.newListRecordsRequest(); + listRecordsRequest.setDateTimeFormatter(dateTimeFormatter); + listRecordsRequest.setFrom(Instant.parse("2016-01-06T00:00:00Z")); + listRecordsRequest.setUntil(Instant.parse("2016-11-07T00:00:00Z")); + listRecordsRequest.setMetadataPrefix("oai_dc"); + final AtomicLong count = new AtomicLong(0L); + SimpleMetadataHandler simpleMetadataHandler = new SimpleMetadataHandler() { + @Override + public void startDocument() throws SAXException { + logger.debug("start doc"); + } + + @Override + public void endDocument() throws SAXException { + logger.debug("end doc"); + count.incrementAndGet(); + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + } + + @Override + public void startElement(String ns, String localname, String qname, Attributes atrbts) throws SAXException { + } + + @Override + public void endElement(String ns, String localname, String qname) throws SAXException { + } + + @Override + public void characters(char[] chars, int pos, int len) throws SAXException { + } + }; + File file = File.createTempFile("doaj.", ".xml"); + file.deleteOnExit(); + FileWriter fileWriter = new FileWriter(file); + do { + try { + listRecordsRequest.addHandler(simpleMetadataHandler); + client = oaiClient.getHttpClient(); + response = client.execute(HttpHeaders.of(HttpMethod.GET, listRecordsRequest.getPath()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + // follow a maximum of 10 HTTP redirects + max = 10; + while (response.followUrl() != null && max-- > 0) { + URI uri = URI.create(response.followUrl()); + client = Clients.newClient(oaiClient.getFactory(), "none+" + uri, HttpClient.class); + response = client.execute(HttpHeaders.of(HttpMethod.GET, response.followUrl()) + .set(HttpHeaderNames.ACCEPT, "utf-8")).aggregate().get(); + } + ListRecordsResponse listRecordsResponse = new ListRecordsResponse(listRecordsRequest); + logger.debug("response = {}", response.headers()); + listRecordsResponse.receivedResponse(response, fileWriter); + listRecordsRequest = oaiClient.resume(listRecordsRequest, listRecordsResponse.getResumptionToken()); + } catch (IOException e) { + logger.error(e.getMessage(), e); + listRecordsRequest = null; + } + } while (listRecordsRequest != null); + fileWriter.close(); + oaiClient.close(); + logger.info("count={}", count.get()); + assertTrue(count.get() > 0L); + } catch (ConnectException | ExecutionException e) { + logger.warn("skipped, can not connect, exception is:", e); + } catch (InterruptedException | IOException e) { + throw e; + } + } + +} diff --git a/oai-client/src/test/java/org/xbib/oai/client/package-info.java b/oai-client/src/test/java/org/xbib/oai/client/package-info.java new file mode 100644 index 0000000..4203250 --- /dev/null +++ b/oai-client/src/test/java/org/xbib/oai/client/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for testing OAI client. + */ +package org.xbib.oai.client; \ No newline at end of file diff --git a/oai-client/src/test/resources/log4j2.xml b/oai-client/src/test/resources/log4j2.xml new file mode 100644 index 0000000..b175dfc --- /dev/null +++ b/oai-client/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/oai-client/src/test/resources/org/xbib/oai/client/DNB.properties b/oai-client/src/test/resources/org/xbib/oai/client/DNB.properties new file mode 100644 index 0000000..5325967 --- /dev/null +++ b/oai-client/src/test/resources/org/xbib/oai/client/DNB.properties @@ -0,0 +1 @@ +uri=http://services.dnb.de/oai/repository diff --git a/oai-client/src/test/resources/org/xbib/oai/client/DOAJ.properties b/oai-client/src/test/resources/org/xbib/oai/client/DOAJ.properties new file mode 100644 index 0000000..0016dd4 --- /dev/null +++ b/oai-client/src/test/resources/org/xbib/oai/client/DOAJ.properties @@ -0,0 +1 @@ +uri=http://doaj.org/oai diff --git a/oai-client/src/test/resources/org/xbib/oai/client/ZDB.properties b/oai-client/src/test/resources/org/xbib/oai/client/ZDB.properties new file mode 100644 index 0000000..5325967 --- /dev/null +++ b/oai-client/src/test/resources/org/xbib/oai/client/ZDB.properties @@ -0,0 +1 @@ +uri=http://services.dnb.de/oai/repository diff --git a/oai-client/src/test/resources/org/xbib/oai/client/doaj-list-records.xml b/oai-client/src/test/resources/org/xbib/oai/client/doaj-list-records.xml new file mode 100644 index 0000000..c89dd38 --- /dev/null +++ b/oai-client/src/test/resources/org/xbib/oai/client/doaj-list-records.xml @@ -0,0 +1,63 @@ + + + 2013-06-19T07:11:37Z + http://doaj.org/oai + + + +

+ oai:doaj.org:2011-9860 + 2013-01-01T12:30:30Z + Biology_and_Life_Sciences +
+ + + Morfolia + http://www.revistas.unal.edu.co/index.php/morfolia + http://www.doaj.org/doaj?func=openurl&genre=journal&issn=20119860 + issn: 2011-9860 + Universidad Nacional de Colombia + 2008 + Spanish + morphology + anatomy + histology + embryology + genetics + DoajSubjectTerm: Biology + + LCC: QH301-705.5 + + + + +
+ oai:doaj.org:1091-1774 + 2013-01-01T01:02:00Z + Social_Sciences +
+ + + Hmong Studies Journal + http://www.hmongstudies.org/HmongStudiesJournal.html + http://www.doaj.org/doaj?func=openurl&genre=journal&issn=10911774 + issn: 1091-1774 + Hmong Studies Journal + 1996 + English + Hmong culture + Hmong history + southeast Asian Americans + Asian American studies + southeast Asian studies + DoajSubjectTerm: Social Sciences + + LCC: H1-99 + LCC: HD28-9999 + + +
+ + + + \ No newline at end of file diff --git a/oai-client/src/test/resources/org/xbib/xml/namespace.properties b/oai-client/src/test/resources/org/xbib/xml/namespace.properties new file mode 100644 index 0000000..0599fcf --- /dev/null +++ b/oai-client/src/test/resources/org/xbib/xml/namespace.properties @@ -0,0 +1,41 @@ +# XML namespace +xml = http://www.w3.org/XML/1998/namespace +xsl = http://www.w3.org/1999/XSL/Transform + +# Atom +atom = http://www.w3.org/2005/Atom + +# RDF namespace +rdf = http://www.w3.org/1999/02/22-rdf-syntax-ns# +rdfs = http://www.w3.org/2000/01/rdf-schema# +owl = http://www.w3.org/2002/07/owl# + +foaf = http://xmlns.com/foaf/0.1/ + +# Apache +xalan = http://xml.apache.org/xslt + +# Dublin Core Namespaces +# http://dublincore.org/documents/dcmi-namespace/ +dc = http://purl.org/dc/elements/1.1/ +dcterms = http://purl.org/dc/terms/ +dcam = http://purl.org/dc/dcam/ +dcmitype http://purl.org/dc/dcmitype/ +rel = http://purl.org/vocab/relationship/ + +# Library of Congress +marcrel = http://www.loc.gov/loc.terms/relators/ +mods = http://www.loc.gov/mods/v3 +bib = info:srw/cql-context-set/1/bib-v1/ + +# RDA, MARC +rdagr2 = http://RDVocab.info/ElementsGr2/ +marclang = http://marccodes.heroku.com/languages/ + +# DNB +gnd = http://d-nb.info/standards/elementset/gnd# +gndvocab = http://d-nb.info/standards/vocab/gnd/ + +# xbib +xbib = http://xbib.org/elements/ + diff --git a/oai-client/src/test/resources/xsl/oai2.xsl b/oai-client/src/test/resources/xsl/oai2.xsl new file mode 100644 index 0000000..c231307 --- /dev/null +++ b/oai-client/src/test/resources/xsl/oai2.xsl @@ -0,0 +1,659 @@ + + + + + + + + + + + + + + + +td.value { + vertical-align: top; + padding-left: 1em; + padding: 3px; +} +td.key { + background-color: #e0e0ff; + padding: 3px; + text-align: right; + border: 1px solid #c0c0c0; + white-space: nowrap; + font-weight: bold; + vertical-align: top; +} +.dcdata td.key { + background-color: #ffffe0; +} +body { + margin: 1em 2em 1em 2em; +} +h1, h2, h3 { + font-family: sans-serif; + clear: left; +} +h1 { + padding-bottom: 4px; + margin-bottom: 0px; +} +h2 { + margin-bottom: 0.5em; +} +h3 { + margin-bottom: 0.3em; + font-size: medium; +} +.link { + border: 1px outset #88f; + background-color: #c0c0ff; + padding: 1px 4px 1px 4px; + font-size: 80%; + text-decoration: none; + font-weight: bold; + font-family: sans-serif; + color: black; +} +.link:hover { + color: red; +} +.link:active { + color: red; + border: 1px inset #88f; + background-color: #a0a0df; +} +.oaiRecord, .oaiRecordTitle { + background-color: #f0f0ff; + border-style: solid; + border-color: #d0d0d0; +} +h2.oaiRecordTitle { + background-color: #e0e0ff; + font-size: medium; + font-weight: bold; + padding: 10px; + border-width: 2px 2px 0px 2px; + margin: 0px; +} +.oaiRecord { + margin-bottom: 3em; + border-width: 2px; + padding: 10px; +} + +.results { + margin-bottom: 1.5em; +} +ul.quicklinks { + margin-top: 2px; + padding: 4px; + text-align: left; + border-bottom: 2px solid #ccc; + border-top: 2px solid #ccc; + clear: left; +} +ul.quicklinks li { + font-size: 80%; + display: inline; + list-stlye: none; + font-family: sans-serif; +} +p.intro { + font-size: 80%; +} + + + + + + + + + OAI 2.0 Request Results + + + +

OAI 2.0 Request Results

+ +

You are viewing an HTML version of the XML OAI response. To see the underlying XML use your web browsers view source option. More information about this XSLT is at the bottom of the page.

+ + +

About the XSLT

+

An XSLT file has converted the OAI-PMH 2.0 responses into XHTML which looks nice in a browser which supports XSLT such as Mozilla, Firebird and Internet Explorer. The XSLT file was created by Christopher Gutteridge at the University of Southampton as part of the GNU EPrints system, and is freely redistributable under the GPL.

If you want to use the XSL file on your own OAI interface you may but due to the way XSLT works you must install the XSL file on the same server as the OAI script, you can't just link to this copy.

For more information or to download the XSL file please see the OAI to XHTML XSLT homepage.

+ + + +
+ + + + + + + + + + + + +
Datestamp of response
Request URL
+ + + +

OAI Error(s)

+

The request could not be completed due to the following error or errors.

+
+ +
+
+ +

Request was of type .

+
+ + + + + + +
+
+
+
+ + + + + + + + +
Error Code
+

+
+ + + + + + + + + + + + + + + + + + +
Repository Name
Base URL
Protocol Version
Earliest Datestamp
Deleted Record Policy
Granularity
+ + +
+ + + Admin Email + + + + + + +

Unsupported Description Type

+

The XSL currently does not support this type of description.

+
+ +
+
+ + + + + +

OAI-Identifier

+ + + + + + + + + +
Scheme
Repository Identifier
Delimiter
Sample OAI Identifier
+
+ + + + + +

EPrints Description

+ +

Content

+ +
+ +

Submission Policy

+ +
+

Metadata Policy

+ +

Data Policy

+ + +
+ + + +

+
+ +
+
+
+ + +

Comment

+
+
+ + + + + +

Friends

+
    + +
+
+ + +
  • + +Identify
  • +
    + + + + + +

    Branding

    + + +
    + + +

    Icon

    + + + {br:title} + + + {br:title} + + +
    + + +

    Metadata Rendering Rule

    + + + + + + + +
    URL
    Namespace
    Mime Type
    +
    + + + + + + +

    Gateway Information

    + + + + + + + + + + + + + + +
    Source
    Description
    URL
    Notes
    +
    + + + Admin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Set

    + + + + +
    setName
    +
    + + + + + + +

    This is a list of metadata formats available for the record "". Use these links to view the metadata:

    +
    + +

    This is a list of metadata formats available from this archive.

    +
    +
    + +
    + + +

    Metadata Format

    + + + + + + + +
    metadataPrefix
    metadataNamespace
    schema
    +
    + + + + + + + + +

    OAI Record:

    +
    + + + +
    +
    + + +

    OAI Record Header

    + + + + + + +
    OAI Identifier + + oai_dc + formats +
    Datestamp
    + +

    This record has been deleted.

    +
    +
    + + + +

    "about" part of record container not supported by the XSL

    +
    + + +   + + + + + + + + + + setSpec + + Identifiers + Records + + + + + + + + +

    There are more results.

    + + + +
    resumptionToken: + +Resume
    +
    + + + + +

    Unknown Metadata Format

    +
    + +
    +
    + + + + +
    +

    Dublin Core Metadata (oai_dc)

    + + +
    +
    +
    + + +Title + + +Author or Creator + + +Subject and Keywords + + +Description + + +Publisher + + +Other Contributor + + +Date + + +Resource Type + + +Format + + +Resource Identifier + + +Source + + +Language + + +Relation + + + + + URL + URL not shown as it is very long. + + + + + + + + + + + + + +Coverage + + +Rights Management + + + + +
    + <></> +
    +
    + + + + + ="" + + + +.xmlSource { + font-size: 70%; + border: solid #c0c0a0 1px; + background-color: #ffffe0; + padding: 2em 2em 2em 0em; +} +.xmlBlock { + padding-left: 2em; +} +.xmlTagName { + color: #800000; + font-weight: bold; +} +.xmlAttrName { + font-weight: bold; +} +.xmlAttrValue { + color: #0000c0; +} + + +
    + diff --git a/oai-common/build.gradle b/oai-common/build.gradle new file mode 100644 index 0000000..6123ecb --- /dev/null +++ b/oai-common/build.gradle @@ -0,0 +1,6 @@ +dependencies { + compile "org.xbib:content-rdf:1.0.3" + //testCompile "xerces:xercesImpl:${versions.xerces}" + //testCompile "xalan:xalan:${versions.xalan}" + //testCompile "com.fasterxml.woodstox:woodstox-core:${versions.woodstox}" +} diff --git a/oai-common/config/checkstyle/checkstyle.xml b/oai-common/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/oai-common/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oai-common/src/main/java/org/xbib/oai/DefaultOAIResponseListener.java b/oai-common/src/main/java/org/xbib/oai/DefaultOAIResponseListener.java new file mode 100644 index 0000000..cf0df94 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/DefaultOAIResponseListener.java @@ -0,0 +1,13 @@ +package org.xbib.oai; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @param response type parameter + */ +public abstract class DefaultOAIResponseListener { + +} diff --git a/oai-common/src/main/java/org/xbib/oai/OAIConstants.java b/oai-common/src/main/java/org/xbib/oai/OAIConstants.java new file mode 100644 index 0000000..4773761 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/OAIConstants.java @@ -0,0 +1,49 @@ +package org.xbib.oai; + +/** + * + */ +public interface OAIConstants { + + String USER_AGENT = "OAI/20161111"; + + String NS_URI = "http://www.openarchives.org/OAI/2.0/"; + + String NS_PREFIX = "oai"; + + String OAIDC_NS_URI = "http://www.openarchives.org/OAI/2.0/oai_dc/"; + + String OAIDC_NS_PREFIX = "oai_dc"; + + String DC_NS_URI = "http://www.purl.org/dc/elements/1.1/"; + + String DC_PREFIX = "dc"; + + String VERB_PARAMETER = "verb"; + + String IDENTIFY = "Identify"; + + String LIST_METADATA_FORMATS = "ListMetadataFormats"; + + String LIST_SETS = "ListSets"; + + String LIST_RECORDS = "ListRecords"; + + String LIST_IDENTIFIERS = "ListIdentifiers"; + + String GET_RECORD = "GetRecord"; + + String FROM_PARAMETER = "from"; + + String UNTIL_PARAMETER = "until"; + + String SET_PARAMETER = "set"; + + String METADATA_PREFIX_PARAMETER = "metadataPrefix"; + + String RESUMPTION_TOKEN_PARAMETER = "resumptionToken"; + + String IDENTIFIER_PARAMETER = "identifier"; + + String REQUEST = "request"; +} diff --git a/oai-common/src/main/java/org/xbib/oai/OAIRequest.java b/oai-common/src/main/java/org/xbib/oai/OAIRequest.java new file mode 100644 index 0000000..2f332b8 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/OAIRequest.java @@ -0,0 +1,23 @@ +package org.xbib.oai; + +import org.xbib.oai.util.ResumptionToken; + +import java.time.Instant; + +/** + * OAI request API. + */ +public interface OAIRequest extends OAIConstants { + + void setSet(String set); + + void setMetadataPrefix(String prefix); + + void setFrom(Instant from); + + void setUntil(Instant until); + + void setResumptionToken(ResumptionToken token); + + ResumptionToken getResumptionToken(); +} diff --git a/oai-common/src/main/java/org/xbib/oai/OAIResponse.java b/oai-common/src/main/java/org/xbib/oai/OAIResponse.java new file mode 100644 index 0000000..1ea8f18 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/OAIResponse.java @@ -0,0 +1,12 @@ +package org.xbib.oai; + +import java.io.IOException; +import java.io.Writer; + +/** + * OAI response. + */ +public interface OAIResponse { + + void to(Writer writer) throws IOException; +} diff --git a/oai-common/src/main/java/org/xbib/oai/OAISession.java b/oai-common/src/main/java/org/xbib/oai/OAISession.java new file mode 100644 index 0000000..cb12c30 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/OAISession.java @@ -0,0 +1,10 @@ +package org.xbib.oai; + +import java.io.Closeable; + +/** + * OAI session. + */ +public interface OAISession extends Closeable { + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/BadArgumentException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/BadArgumentException.java new file mode 100644 index 0000000..ac7d443 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/BadArgumentException.java @@ -0,0 +1,17 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class BadArgumentException extends OAIException { + + private static final long serialVersionUID = -6647892792394074500L; + + public BadArgumentException() { + this(null); + } + + public BadArgumentException(String message) { + super(message); + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/BadResumptionTokenException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/BadResumptionTokenException.java new file mode 100644 index 0000000..7a33197 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/BadResumptionTokenException.java @@ -0,0 +1,15 @@ +package org.xbib.oai.exceptions; + +import org.xbib.oai.util.ResumptionToken; + +/** + * + */ +public class BadResumptionTokenException extends OAIException { + + private static final long serialVersionUID = 7384401627260164303L; + + public BadResumptionTokenException(ResumptionToken token) { + super(token != null ? token.toString() : null); + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/BadVerbException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/BadVerbException.java new file mode 100644 index 0000000..47fcb42 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/BadVerbException.java @@ -0,0 +1,13 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class BadVerbException extends OAIException { + + private static final long serialVersionUID = 1642129565793325510L; + + public BadVerbException(String message) { + super(message); + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/CannotDisseminateFormatException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/CannotDisseminateFormatException.java new file mode 100644 index 0000000..12a9b0e --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/CannotDisseminateFormatException.java @@ -0,0 +1,14 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class CannotDisseminateFormatException extends OAIException { + + private static final long serialVersionUID = 154900133710811545L; + + public CannotDisseminateFormatException(String message) { + super(message); + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/IdDoesNotExistException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/IdDoesNotExistException.java new file mode 100644 index 0000000..a9025a9 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/IdDoesNotExistException.java @@ -0,0 +1,14 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class IdDoesNotExistException extends OAIException { + + private static final long serialVersionUID = 9201985582562843506L; + + public IdDoesNotExistException(String message) { + super(message); + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/NoRecordsMatchException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/NoRecordsMatchException.java new file mode 100644 index 0000000..708c343 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/NoRecordsMatchException.java @@ -0,0 +1,14 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class NoRecordsMatchException extends OAIException { + + private static final long serialVersionUID = 5201331168058463772L; + + public NoRecordsMatchException(String message) { + super(message); + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/NoSetHierarchyException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/NoSetHierarchyException.java new file mode 100644 index 0000000..dfa1abd --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/NoSetHierarchyException.java @@ -0,0 +1,14 @@ +package org.xbib.oai.exceptions; + +/** + * + */ +public class NoSetHierarchyException extends OAIException { + + private static final long serialVersionUID = 6275260694745177314L; + + public NoSetHierarchyException(String message) { + super(message); + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/OAIException.java b/oai-common/src/main/java/org/xbib/oai/exceptions/OAIException.java new file mode 100644 index 0000000..380ce01 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/OAIException.java @@ -0,0 +1,24 @@ +package org.xbib.oai.exceptions; + +import java.io.IOException; + +/** + * + */ +public class OAIException extends IOException { + + private static final long serialVersionUID = -1890146067179892744L; + + public OAIException(String message) { + super(message); + } + + public OAIException(Throwable throwable) { + super(throwable); + } + + public OAIException(String message, Throwable throwable) { + super(message, throwable); + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/exceptions/package-info.java b/oai-common/src/main/java/org/xbib/oai/exceptions/package-info.java new file mode 100644 index 0000000..e05b334 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/exceptions/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI exceptions. + */ +package org.xbib.oai.exceptions; diff --git a/oai-common/src/main/java/org/xbib/oai/package-info.java b/oai-common/src/main/java/org/xbib/oai/package-info.java new file mode 100644 index 0000000..d748a7a --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI. + */ +package org.xbib.oai; diff --git a/oai-common/src/main/java/org/xbib/oai/rdf/RdfResourceHandler.java b/oai-common/src/main/java/org/xbib/oai/rdf/RdfResourceHandler.java new file mode 100644 index 0000000..1156c73 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/rdf/RdfResourceHandler.java @@ -0,0 +1,59 @@ +package org.xbib.oai.rdf; + +import org.xbib.content.rdf.RdfContentParams; +import org.xbib.content.rdf.io.xml.AbstractXmlResourceHandler; +import org.xbib.content.rdf.io.xml.XmlHandler; +import org.xbib.content.resource.IRI; +import org.xbib.content.resource.IRINamespaceContext; +import org.xbib.oai.OAIConstants; + +import javax.xml.namespace.QName; + +/** + * A default RDF resource handler for OAI. + */ +public class RdfResourceHandler extends AbstractXmlResourceHandler implements OAIConstants { + + public RdfResourceHandler(RdfContentParams params) { + super(params); + } + + @Override + public void identify(QName name, String value, IRI identifier) { + // do nothing + } + + @Override + public boolean isResourceDelimiter(QName name) { + boolean b = OAIDC_NS_URI.equals(name.getNamespaceURI()) + && DC_PREFIX.equals(name.getLocalPart()); + return b; + } + + @Override + public boolean skip(QName name) { + boolean b = OAIDC_NS_URI.equals(name.getNamespaceURI()) + && DC_PREFIX.equals(name.getLocalPart()); + b = b || name.getLocalPart().startsWith("@"); + return b; + } + + @Override + public void addToPredicate(QName parent, String content) { + // do nothing + } + + public Object toObject(QName parent, String content) { + return content; + } + + @Override + public XmlHandler setNamespaceContext(IRINamespaceContext namespaceContext) { + return this; + } + + @Override + public IRINamespaceContext getNamespaceContext() { + return getParams().getNamespaceContext(); + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/rdf/RdfSimpleMetadataHandler.java b/oai-common/src/main/java/org/xbib/oai/rdf/RdfSimpleMetadataHandler.java new file mode 100644 index 0000000..0fd7fee --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/rdf/RdfSimpleMetadataHandler.java @@ -0,0 +1,140 @@ +package org.xbib.oai.rdf; + +import org.xbib.content.rdf.RdfContentBuilder; +import org.xbib.content.rdf.RdfContentParams; +import org.xbib.content.rdf.Resource; +import org.xbib.content.rdf.internal.DefaultAnonymousResource; +import org.xbib.content.rdf.io.xml.XmlResourceHandler; +import org.xbib.content.resource.IRI; +import org.xbib.content.resource.IRINamespaceContext; +import org.xbib.oai.OAIConstants; +import org.xbib.oai.xml.SimpleMetadataHandler; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import java.io.IOException; + +/** + * RDF metadata handler. + */ +public class RdfSimpleMetadataHandler extends SimpleMetadataHandler implements OAIConstants { + + private RdfResourceHandler handler; + + private Resource resource; + + private RdfContentBuilder builder; + + private RdfContentParams params; + + public static IRINamespaceContext getDefaultContext() { + IRINamespaceContext context = IRINamespaceContext.newInstance(); + context.addNamespace(DC_PREFIX, DC_NS_URI); + context.addNamespace(OAIDC_NS_PREFIX, OAIDC_NS_URI); + return context; + } + + public RdfSimpleMetadataHandler() { + this(RdfSimpleMetadataHandler::getDefaultContext); + } + + public RdfSimpleMetadataHandler(RdfContentParams params) { + this.params = params; + this.resource = new DefaultAnonymousResource(); + // set up our default handler + this.handler = new RdfResourceHandler(params); + handler.setDefaultNamespace(NS_PREFIX, NS_URI); + } + + public IRINamespaceContext getContext() { + return params.getNamespaceContext(); + } + + public Resource getResource() { + return resource; + } + + public RdfSimpleMetadataHandler setHandler(RdfResourceHandler handler) { + handler.setDefaultNamespace(NS_PREFIX, NS_URI); + this.handler = handler; + return this; + } + + public XmlResourceHandler getHandler() { + return handler; + } + + public RdfSimpleMetadataHandler setBuilder(RdfContentBuilder builder) { + this.builder = builder; + return this; + } + + @Override + public void startDocument() throws SAXException { + if (handler != null) { + handler.startDocument(); + } + } + + /** + * At the endStream of each OAI metadata, the resource context receives the identifier from + * the metadata header. The resource context is pushed to the RDF output. + * Any IOException is converted to a SAXException. + * + * @throws SAXException if SaX fails + */ + @Override + public void endDocument() throws SAXException { + String id = getHeader().getIdentifier().trim(); + if (handler != null) { + handler.identify(null, id, null); + resource.setId(IRI.create(id)); + handler.endDocument(); + try { + if (builder != null) { + builder.receive(resource); + } + } catch (IOException e) { + throw new SAXException(e); + } + } + } + + @Override + public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { + if (handler != null) { + handler.startPrefixMapping(prefix, namespaceURI); + if (prefix.isEmpty()) { + handler.setDefaultNamespace("oai", namespaceURI); + } + } + } + + @Override + public void endPrefixMapping(String string) throws SAXException { + if (handler != null) { + handler.endPrefixMapping(string); + } + } + + @Override + public void startElement(String ns, String localname, String string2, Attributes atrbts) throws SAXException { + if (handler != null) { + handler.startElement(ns, localname, string2, atrbts); + } + } + + @Override + public void endElement(String ns, String localname, String string2) throws SAXException { + if (handler != null) { + handler.endElement(ns, localname, string2); + } + } + + @Override + public void characters(char[] chars, int i, int i1) throws SAXException { + if (handler != null) { + handler.characters(chars, i, i1); + } + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/rdf/package-info.java b/oai-common/src/main/java/org/xbib/oai/rdf/package-info.java new file mode 100644 index 0000000..1d0eb21 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/rdf/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for RDF in OAI. + */ +package org.xbib.oai.rdf; diff --git a/oai-common/src/main/java/org/xbib/oai/util/RecordHeader.java b/oai-common/src/main/java/org/xbib/oai/util/RecordHeader.java new file mode 100644 index 0000000..3a1acdc --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/util/RecordHeader.java @@ -0,0 +1,42 @@ +package org.xbib.oai.util; + +import java.time.Instant; + +/** + * + */ +public class RecordHeader { + + private String identifier; + + private Instant date; + + private String set; + + public RecordHeader setIdentifier(String identifier) { + this.identifier = identifier; + return this; + } + + public String getIdentifier() { + return identifier; + } + + public RecordHeader setDate(Instant date) { + this.date = date; + return this; + } + + public Instant getDate() { + return date; + } + + public RecordHeader setSetspec(String setSpec) { + this.set = setSpec; + return this; + } + + public String getSetSpec() { + return set; + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/util/ResumptionToken.java b/oai-common/src/main/java/org/xbib/oai/util/ResumptionToken.java new file mode 100644 index 0000000..578046f --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/util/ResumptionToken.java @@ -0,0 +1,171 @@ +package org.xbib.oai.util; + +import java.util.Date; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * + * @param token parameter type + */ +public class ResumptionToken { + + private static final int DEFAULT_INTERVAL_SIZE = 1000; + + private static final ConcurrentHashMap> cache = new ConcurrentHashMap<>(); + + private final UUID uuid; + + private final int interval; + + private int position; + + private T value; + + private Date expirationDate; + + private int completeListSize; + + private int cursor; + + private String metadataPrefix; + + private String set; + + private Date from; + + private Date until; + + private boolean completed; + + private ResumptionToken() { + this(DEFAULT_INTERVAL_SIZE); + this.completed = false; + } + + private ResumptionToken(int interval) { + this.uuid = UUID.randomUUID(); + this.position = 0; + this.interval = interval; + this.value = null; + cache.put(uuid, this); + } + + public static ResumptionToken newToken(T value) { + return new ResumptionToken().setValue(value); + } + + public static ResumptionToken get(UUID token) { + return cache.get(token); + } + + public UUID getKey() { + return uuid; + } + + public ResumptionToken setPosition(int position) { + this.position = position; + return this; + } + + public int getPosition() { + return position; + } + + public int advancePosition() { + setPosition(position + interval); + return getPosition(); + } + + public int getInterval() { + return interval; + } + + public ResumptionToken setValue(T value) { + this.value = value; + return this; + } + + public T getValue() { + return value; + } + + public ResumptionToken setExpirationDate(Date date) { + this.expirationDate = date; + return this; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public ResumptionToken setCompleteListSize(int size) { + this.completeListSize = size; + completed = size < interval; + return this; + } + + public int getCompleteListSize() { + return completeListSize; + } + + public ResumptionToken setCursor(int cursor) { + this.cursor = cursor; + return this; + } + + public int getCursor() { + return cursor; + } + + public ResumptionToken setMetadataPrefix(String metadataPrefix) { + this.metadataPrefix = metadataPrefix; + return this; + } + + public String getMetadataPrefix() { + return metadataPrefix; + } + + public ResumptionToken setSet(String set) { + this.set = set; + return this; + } + + public String getSet() { + return set; + } + + public ResumptionToken setFrom(Date from) { + this.from = from; + return this; + } + + public Date getFrom() { + return from; + } + + public ResumptionToken setUntil(Date until) { + this.until = until; + return this; + } + + public Date getUntil() { + return until; + } + + public void update(int completeListSize, int pageSize, int currentPage) { + this.completeListSize = completeListSize; + this.cursor = pageSize * currentPage; + } + + public boolean isComplete() { + return completed; + } + + @Override + public String toString() { + return value != null ? value.toString() : null; + } +} + diff --git a/oai-common/src/main/java/org/xbib/oai/util/URIBuilder.java b/oai-common/src/main/java/org/xbib/oai/util/URIBuilder.java new file mode 100644 index 0000000..c22b613 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/util/URIBuilder.java @@ -0,0 +1,230 @@ +package org.xbib.oai.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * + */ +public class URIBuilder { + + private URI uri; + private String scheme; + private String authority; + private String path; + private String fragment; + private Map params; + + public URIBuilder() { + this.params = new LinkedHashMap<>(); + } + + public URIBuilder(String base) { + this(URI.create(base)); + } + + public URIBuilder(URI base) { + this.uri = base; + this.scheme = uri.getScheme(); + this.authority = uri.getAuthority(); + this.path = uri.getPath(); + this.fragment = uri.getFragment(); + this.params = parseQueryString(uri); + } + + public URIBuilder(URI base, Charset encoding) { + this.uri = base; + this.params = parseQueryString(uri, encoding); + } + + public URIBuilder scheme(String scheme) { + this.scheme = scheme; + return this; + } + + public URIBuilder authority(String authority) { + this.authority = authority; + return this; + } + + public URIBuilder path(String path) { + this.path = path; + return this; + } + + public URIBuilder fragment(String fragment) { + this.fragment = fragment; + return this; + } + /** + * This method adds a single key/value parameter to the query + * string of a given URI. Existing keys will be overwritten. + * + * @param key the key + * @param value the value + * @return this URI builder + */ + public URIBuilder addParameter(String key, String value) { + params.put(key, value); + return this; + } + + public String buildGetPath() { + return path + (params.isEmpty() ? "" : "?" + URIFormatter.renderQueryString(params)); + } + + /** + * This method adds a single key/value parameter to the query + * string of a given URI, URI-escaped with the given encoding. + * Existing keys will be overwritten. + * + * @param key the key + * @param value the value + * @param encoding the encoding + * @return this URI builder + */ + public URIBuilder addParameter(String key, String value, Charset encoding) { + params.put(key, URIFormatter.encode(value, encoding)); + return this; + } + + public URI build() { + try { + return new URI(scheme, authority, path, URIFormatter.renderQueryString(params), fragment); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public URI build(Charset encoding) { + try { + return new URI(scheme, authority, path, URIFormatter.renderQueryString(params, encoding), fragment); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * This method parses a query string and returns a map of decoded + * request parameters. We do not rely on java.net.URI because it does not + * decode plus characters. The encoding is UTF-8. + * + * @param uri the URI to examine for request parameters + * @return a map + */ + public static Map parseQueryString(URI uri) { + return parseQueryString(uri, StandardCharsets.UTF_8); + } + + /** + * This method parses a query string and returns a map of decoded + * request parameters. We do not rely on java.net.URI because it does not + * decode plus characters. + * + * @param uri the URI to examine for request parameters + * @param encoding the encoding + * @return a Map + */ + public static Map parseQueryString(URI uri, Charset encoding) { + return parseQueryString(uri, encoding, null); + } + + /** + * This method parses a query string and returns a map of decoded + * request parameters. We do not rely on java.net.URI because it does not + * decode plus characters. A listener can process the parameters in order. + * + * @param uri the URI to examine for request parameters + * @param encoding the encoding + * @param listener a listner for processing the URI parameters in order, or null + * @return a Map of parameters + */ + public static Map parseQueryString(URI uri, Charset encoding, ParameterListener listener) { + if (uri == null) { + throw new IllegalArgumentException(); + } + return parseQueryString(uri.getRawQuery(), encoding, listener); + } + + public static Map parseQueryString(String rawQuery, Charset encoding, ParameterListener listener) { + Map m = new HashMap<>(); + if (rawQuery == null) { + return m; + } + // we use getRawQuery because we do our decoding by ourselves + StringTokenizer st = new StringTokenizer(rawQuery, "&"); + while (st.hasMoreTokens()) { + String pair = st.nextToken(); + String k; + String v; + int pos = pair.indexOf('='); + if (pos < 0) { + k = pair; + v = null; + } else { + k = pair.substring(0, pos); + v = decode(pair.substring(pos + 1, pair.length()), encoding); + } + m.put(k, v); + if (listener != null) { + listener.received(k, v); + } + } + return m; + } + + /** + * Decodes an octet according to RFC 2396. According to this spec, + * any characters outside the range 0x20 - 0x7E must be escaped because + * they are not printable characters, except for any characters in the + * fragment identifier. This method will translate any escaped characters + * back to the original. + * + * @param octet the octet to decode + * @param encoding the encoding to decode into + * @return The decoded URI + */ + public static String decode(String octet, Charset encoding) { + StringBuilder sb = new StringBuilder(); + boolean fragment = false; + for (int i = 0; i < octet.length(); i++) { + char ch = octet.charAt(i); + switch (ch) { + case '+': + sb.append(' '); + break; + case '#': + sb.append(ch); + fragment = true; + break; + case '%': + if (!fragment) { + // fast hex decode + sb.append((char) ((Character.digit(octet.charAt(++i), 16) << 4) + | Character.digit(octet.charAt(++i), 16))); + } else { + sb.append(ch); + } + break; + default: + sb.append(ch); + break; + } + } + return new String(sb.toString().getBytes(StandardCharsets.ISO_8859_1), encoding); + } + + /** + * + */ + @FunctionalInterface + public interface ParameterListener { + void received(String k, String v); + } +} diff --git a/oai-common/src/main/java/org/xbib/oai/util/URIFormatter.java b/oai-common/src/main/java/org/xbib/oai/util/URIFormatter.java new file mode 100644 index 0000000..fa0a180 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/util/URIFormatter.java @@ -0,0 +1,138 @@ +package org.xbib.oai.util; + +import java.nio.charset.Charset; +import java.util.Map; + +/** + * + */ +public class URIFormatter { + + public static String renderQueryString(Map m) { + return renderQueryString(m, null, false); + } + + public static String renderQueryString(Map m, Charset encoding) { + return renderQueryString(m, encoding, true); + } + + /** + * This method takes a Map of key/value elements and converts it + * into a URL encoded querystring format. + * + * @param m a map of key/value arrays + * @param encoding the charset + * @param encode true if arameter must be encoded + * @return a string with the URL encoded data + */ + public static String renderQueryString(Map m, Charset encoding, boolean encode) { + String key; + String value; + StringBuilder out = new StringBuilder(); + for (Map.Entry me : m.entrySet()) { + key = me.getKey(); + value = encode ? encode(me.getValue(), encoding) : me.getValue(); + if (key != null) { + if (out.length() > 0) { + out.append("&"); + } + out.append(key); + if ((value != null) && (value.length() > 0)) { + out.append("=").append(value); + } + } + } + return out.toString(); + } + + /** + *

    Encode a string into URI syntax

    + *

    This function applies the URI escaping rules defined in + * section 2 of [RFC 2396], as amended by [RFC 2732], to the string + * supplied as the first argument, which typically represents all or part + * of a URI, URI reference or IRI. The effect of the function is to + * replace any special character in the string by an escape sequence of + * the form %xx%yy..., where xxyy... is the hexadecimal representation of + * the octets used to represent the character in US-ASCII for characters + * in the ASCII repertoire, and a different character encoding for + * non-ASCII characters.

    + *

    If the second argument is true, all characters are escaped + * other than lower case letters a-z, upper case letters A-Z, digits 0-9, + * and the characters referred to in [RFC 2396] as "marks": specifically, + * "-" | "_" | "." | "!" | "~" | "" | "'" | "(" | ")". The "%" character + * itself is escaped only if it is not followed by two hexadecimal digits + * (that is, 0-9, a-f, and A-F).

    + *

    [RFC 2396] does not define whether escaped URIs should use + * lower case or upper case for hexadecimal digits. To ensure that escaped + * URIs can be compared using string comparison functions, this function + * must always use the upper-case letters A-F.

    + *

    The character encoding used as the basis for determining the + * octets depends on the setting of the second argument.

    + * + * @param s the String to convert + * @param encoding The encoding to use for unsafe characters + * @return The converted String + */ + public static String encode(String s, Charset encoding) { + if (s == null) { + return null; + } + int length = s.length(); + int start = 0; + int i = 0; + StringBuilder result = new StringBuilder(length); + while (true) { + while ((i < length) && isSafe(s.charAt(i))) { + i++; + } + // Safe character can just be added + result.append(s.substring(start, i)); + // Are we done? + if (i >= length) { + return result.toString(); + } else if (s.charAt(i) == ' ') { + result.append('+'); // Replace space char with plus symbol. + i++; + } else { + // Get all unsafe characters + start = i; + char c; + while ((i < length) && ((c = s.charAt(i)) != ' ') && !isSafe(c)) { + i++; + } + // Convert them to %XY encoded strings + String unsafe = s.substring(start, i); + byte[] bytes = unsafe.getBytes(encoding); + for (byte aByte : bytes) { + result.append('%'); + result.append(hex.charAt(((int) aByte & 0xf0) >> 4)); + result.append(hex.charAt((int) aByte & 0x0f)); + } + } + start = i; + } + } + + /** + * Returns true if the given char is + * either a uppercase or lowercase letter from 'a' till 'z', or a digit + * froim '0' till '9', or one of the characters '-', '_', '.' or ''. Such + * 'safe' character don't have to be url encoded. + * + * @param c the character + * @return true or false + */ + private static boolean isSafe(char c) { + return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) + || ((c >= '0') && (c <= '9')) || (c == '-') || (c == '_') || (c == '.') || (c == '*')); + } + + /** + * Used to convert to hex. We don't use Integer.toHexString, since + * it converts to lower case (and the Sun docs pretty clearly specify + * upper case here), and because it doesn't provide a leading 0. + */ + private static final String hex = "0123456789ABCDEF"; + + +} diff --git a/oai-common/src/main/java/org/xbib/oai/util/package-info.java b/oai-common/src/main/java/org/xbib/oai/util/package-info.java new file mode 100644 index 0000000..bf7f288 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/util/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI utilities. + */ +package org.xbib.oai.util; diff --git a/oai-common/src/main/java/org/xbib/oai/xml/MetadataHandler.java b/oai-common/src/main/java/org/xbib/oai/xml/MetadataHandler.java new file mode 100644 index 0000000..ea02de4 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/xml/MetadataHandler.java @@ -0,0 +1,14 @@ +package org.xbib.oai.xml; + +import org.xbib.oai.util.RecordHeader; +import org.xml.sax.ContentHandler; + +/** + * + */ +public interface MetadataHandler extends ContentHandler { + + MetadataHandler setHeader(RecordHeader header); + + RecordHeader getHeader(); +} diff --git a/oai-common/src/main/java/org/xbib/oai/xml/SimpleMetadataHandler.java b/oai-common/src/main/java/org/xbib/oai/xml/SimpleMetadataHandler.java new file mode 100644 index 0000000..76fd9a2 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/xml/SimpleMetadataHandler.java @@ -0,0 +1,22 @@ +package org.xbib.oai.xml; + +import org.xbib.content.xml.util.XMLFilterReader; +import org.xbib.oai.util.RecordHeader; + +/** + * + */ +public class SimpleMetadataHandler extends XMLFilterReader implements MetadataHandler { + + private RecordHeader header; + + public SimpleMetadataHandler setHeader(RecordHeader header) { + this.header = header; + return this; + } + + public RecordHeader getHeader() { + return header; + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/xml/XmlSimpleMetadataHandler.java b/oai-common/src/main/java/org/xbib/oai/xml/XmlSimpleMetadataHandler.java new file mode 100644 index 0000000..d12ca43 --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/xml/XmlSimpleMetadataHandler.java @@ -0,0 +1,297 @@ +package org.xbib.oai.xml; + +import org.xbib.oai.OAIConstants; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import javax.xml.stream.Location; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Namespace; + +/** + * + */ +public class XmlSimpleMetadataHandler extends SimpleMetadataHandler implements OAIConstants { + + private final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + + private final XMLEventFactory eventFactory = XMLEventFactory.newInstance(); + + private List namespaces = new ArrayList<>(); + + private Stack> nsStack = new Stack<>(); + + private Locator locator; + + private XMLEventWriter eventWriter; + + private Writer writer; + + private String id; + + private boolean needToCallStartDocument = false; + + public XmlSimpleMetadataHandler setWriter(Writer writer) { + this.writer = writer; + try { + outputFactory.setProperty("javax.xml.stream.isRepairingNamespaces", Boolean.TRUE); + this.eventWriter = outputFactory.createXMLEventWriter(writer); + } catch (XMLStreamException e) { + // ignore + } + return this; + } + + public Writer getWriter() { + return writer; + } + + public XmlSimpleMetadataHandler setEventWriter(XMLEventWriter eventWriter) { + this.eventWriter = eventWriter; + return this; + } + + public XMLEventWriter getEventWriter() { + return eventWriter; + } + + public String getIdentifier() { + return id; + } + + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + public Location getCurrentLocation() { + if (locator != null) { + return new SAXLocation(locator); + } else { + return null; + } + } + + @Override + public void startDocument() throws SAXException { + if (eventWriter == null) { + return; + } + namespaces.clear(); + nsStack.clear(); + eventFactory.setLocation(getCurrentLocation()); + needToCallStartDocument = true; + } + + @Override + public void endDocument() throws SAXException { + if (eventWriter == null) { + return; + } + this.id = getHeader().getIdentifier().trim(); + try { + eventFactory.setLocation(getCurrentLocation()); + eventWriter.add(eventFactory.createEndDocument()); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + namespaces.clear(); + nsStack.clear(); + } + + @Override + public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { + if (eventWriter == null) { + return; + } + if (prefix == null) { + prefix = ""; + } else if (prefix.equals("xml")) { + return; + } + if (namespaces == null) { + namespaces = new ArrayList<>(); + } + namespaces.add(prefix); + namespaces.add(namespaceURI); + } + + @Override + public void endPrefixMapping(String string) throws SAXException { + } + + @Override + public void startElement(String uri, String localname, String qname, Attributes attributes) throws SAXException { + if (eventWriter == null) { + return; + } + if (needToCallStartDocument) { + try { + eventWriter.add(eventFactory.createStartDocument()); + } catch (XMLStreamException e) { + // is thrown because of document encoding - commented out + //throw new SAXException(e); + } + needToCallStartDocument = false; + } + Collection[] events = {null, null}; + createStartEvents(attributes, events); + nsStack.add(events[0]); + try { + String[] q = {null, null}; + parseQName(qname, q); + eventFactory.setLocation(getCurrentLocation()); + eventWriter.add(eventFactory.createStartElement(q[0], uri, + q[1], events[1].iterator(), events[0].iterator())); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void endElement(String uri, String localname, String qname) throws SAXException { + if (eventWriter == null) { + return; + } + String[] q = {null, null}; + parseQName(qname, q); + Collection nsList = nsStack.remove(nsStack.size() - 1); + Iterator nsIter = nsList.iterator(); + try { + eventFactory.setLocation(getCurrentLocation()); + eventWriter.add(eventFactory.createEndElement(q[0], uri, q[1], nsIter)); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + @Override + public void characters(char[] chars, int i, int i1) throws SAXException { + if (eventWriter == null) { + return; + } + try { + eventFactory.setLocation(getCurrentLocation()); + eventWriter.add(eventFactory.createCharacters(new String(chars, i, i1))); + } catch (XMLStreamException e) { + throw new SAXException(e); + } + } + + private void createStartEvents(Attributes attributes, Collection[] events) { + Map nsMap = null; + List attrs = null; + if (namespaces != null) { + final int nDecls = namespaces.size(); + for (int i = 0; i < nDecls; i++) { + final String prefix = namespaces.get(i); + String uri = namespaces.get(i++); + Namespace ns = createNamespace(prefix, uri); + if (nsMap == null) { + nsMap = new HashMap<>(); + } + nsMap.put(prefix, ns); + } + } + String[] qname = {null, null}; + for (int i = 0, s = attributes.getLength(); i < s; i++) { + parseQName(attributes.getQName(i), qname); + String attrPrefix = qname[0]; + String attrLocal = qname[1]; + String attrQName = attributes.getQName(i); + String attrValue = attributes.getValue(i); + String attrURI = attributes.getURI(i); + if ("xmlns".equals(attrQName) || "xmlns".equals(attrPrefix)) { + if (!attrValue.isEmpty() && nsMap != null && !nsMap.containsKey(attrPrefix)) { + Namespace ns = createNamespace(attrPrefix, attrValue); + nsMap = new HashMap<>(); + nsMap.put(attrPrefix, ns); + } + } else { + Attribute attribute; + if (attrPrefix.length() > 0 && !attrValue.isEmpty()) { + attribute = eventFactory.createAttribute(attrPrefix, attrURI, attrLocal, attrValue); + } else { + attribute = eventFactory.createAttribute(attrLocal, attrValue); + } + if (attrs == null) { + attrs = new ArrayList<>(); + } + attrs.add(attribute); + } + } + events[0] = nsMap == null ? Collections.EMPTY_LIST : nsMap.values(); + events[1] = attrs == null ? Collections.EMPTY_LIST : attrs; + } + + private void parseQName(String qName, String[] results) { + String prefix, local; + int idx = qName.indexOf(':'); + if (idx >= 0) { + prefix = qName.substring(0, idx); + local = qName.substring(idx + 1); + } else { + prefix = ""; + local = qName; + } + results[0] = prefix; + results[1] = local; + } + + private Namespace createNamespace(String prefix, String uri) { + if (prefix == null || prefix.length() == 0) { + return eventFactory.createNamespace(uri); + } else { + return eventFactory.createNamespace(prefix, uri); + } + } + + private static final class SAXLocation implements Location { + private int lineNumber; + private int columnNumber; + private String publicId; + private String systemId; + + private SAXLocation(Locator locator) { + lineNumber = locator.getLineNumber(); + columnNumber = locator.getColumnNumber(); + publicId = locator.getPublicId(); + systemId = locator.getSystemId(); + } + + public int getLineNumber() { + return lineNumber; + } + + public int getColumnNumber() { + return columnNumber; + } + + public int getCharacterOffset() { + return -1; + } + + public String getPublicId() { + return publicId; + } + + public String getSystemId() { + return systemId; + } + } + +} diff --git a/oai-common/src/main/java/org/xbib/oai/xml/package-info.java b/oai-common/src/main/java/org/xbib/oai/xml/package-info.java new file mode 100644 index 0000000..443193a --- /dev/null +++ b/oai-common/src/main/java/org/xbib/oai/xml/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI XML processing. + */ +package org.xbib.oai.xml; diff --git a/oai-server/build.gradle b/oai-server/build.gradle new file mode 100644 index 0000000..867ba01 --- /dev/null +++ b/oai-server/build.gradle @@ -0,0 +1,4 @@ +dependencies { + compile project(':oai-common') + compile "org.xbib.helianthus:helianthus-server:1.0.3" +} \ No newline at end of file diff --git a/oai-server/config/checkstyle/checkstyle.xml b/oai-server/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/oai-server/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/oai-server/src/main/java/org/xbib/oai/server/OAIServer.java b/oai-server/src/main/java/org/xbib/oai/server/OAIServer.java new file mode 100644 index 0000000..d6db798 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/OAIServer.java @@ -0,0 +1,120 @@ +package org.xbib.oai.server; + +import org.xbib.oai.OAISession; +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.getrecord.GetRecordServerRequest; +import org.xbib.oai.server.getrecord.GetRecordServerResponse; +import org.xbib.oai.server.identify.IdentifyServerRequest; +import org.xbib.oai.server.identify.IdentifyServerResponse; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerRequest; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerResponse; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerRequest; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerResponse; +import org.xbib.oai.server.listrecords.ListRecordsServerRequest; +import org.xbib.oai.server.listrecords.ListRecordsServerResponse; +import org.xbib.oai.server.listsets.ListSetsServerRequest; +import org.xbib.oai.server.listsets.ListSetsServerResponse; + +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Date; + +/** + * OAI server. + */ +public interface OAIServer { + + URL getURL(); + + OAISession newSession() throws URISyntaxException; + + /** + * This verb is used to retrieve information about a repository. + * Some of the information returned is required as part of the OAI-PMH. + * Repositories may also employ the Identify verb to return additional + * descriptive information. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void identify(IdentifyServerRequest request, IdentifyServerResponse response) throws OAIException; + + /** + * This verb is an abbreviated form of ListRecords, retrieving only + * headers rather than records. Optional arguments permit selective + * harvesting of headers based on set membership and/or datestamp. + * Depending on the repository's support for deletions, a returned + * header may have a status attribute of "deleted" if a record + * matching the arguments specified in the request has been deleted. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void listIdentifiers(ListIdentifiersServerRequest request, ListIdentifiersServerResponse response) throws OAIException; + + /** + * This verb is used to retrieve the metadata formats available + * from a repository. An optional argument restricts the request + * to the formats available for a specific item. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void listMetadataFormats(ListMetadataFormatsServerRequest request, ListMetadataFormatsServerResponse response) + throws OAIException; + + /** + * This verb is used to retrieve the set structure of a repository, + * useful for selective harvesting. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void listSets(ListSetsServerRequest request, ListSetsServerResponse response) throws OAIException; + + /** + * This verb is used to harvest records from a repository. + * Optional arguments permit selective harvesting of records based on + * set membership and/or datestamp. Depending on the repository's + * support for deletions, a returned header may have a status + * attribute of "deleted" if a record matching the arguments + * specified in the request has been deleted. No metadata + * will be present for records with deleted status. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void listRecords(ListRecordsServerRequest request, ListRecordsServerResponse response) throws OAIException; + + /** + * This verb is used to retrieve an individual metadata record from + * a repository. Required arguments specify the identifier of the item + * from which the record is requested and the format of the metadata + * that should be included in the record. Depending on the level at + * which a repository tracks deletions, a header with a "deleted" value + * for the status attribute may be returned, in case the metadata format + * specified by the metadataPrefix is no longer available from the + * repository or from the specified item. + * @param request request + * @param response response + * @throws OAIException if verb fails + */ + void getRecord(GetRecordServerRequest request, GetRecordServerResponse response) throws OAIException; + + Date getLastModified(); + + String getRepositoryName(); + + URL getBaseURL(); + + String getProtocolVersion(); + + String getAdminEmail(); + + String getEarliestDatestamp(); + + String getDeletedRecord(); + + String getGranularity(); + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/OAIServiceFactory.java b/oai-server/src/main/java/org/xbib/oai/server/OAIServiceFactory.java new file mode 100644 index 0000000..227ccb0 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/OAIServiceFactory.java @@ -0,0 +1,58 @@ +package org.xbib.oai.server; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; + +/** + * + */ +public class OAIServiceFactory { + + private static final Map services = new HashMap<>(); + + private static final OAIServiceFactory instance = new OAIServiceFactory(); + + private OAIServiceFactory() { + ServiceLoader loader = ServiceLoader.load(OAIServer.class); + for (OAIServer service : loader) { + if (!services.containsKey(service.getURL())) { + services.put(service.getURL(), service); + } + } + } + + public static OAIServiceFactory getInstance() { + return instance; + } + + public static OAIServer getDefaultService() { + return services.isEmpty() ? null : services.entrySet().iterator().next().getValue(); + } + + public static OAIServer getService(URL url) { + if (services.containsKey(url)) { + return services.get(url); + } + throw new IllegalArgumentException("OAI service " + url + " not found in " + services); + } + + public static OAIServer getService(String name) { + Properties properties = new Properties(); + InputStream in = instance.getClass().getResourceAsStream("/org/xbib/oai/service/" + name + ".properties"); + if (in != null) { + try { + properties.load(in); + } catch (IOException ex) { + // ignore + } + } else { + throw new IllegalArgumentException("service " + name + " not found"); + } + return new PropertiesOAIServer(properties); + } +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/PropertiesOAIServer.java b/oai-server/src/main/java/org/xbib/oai/server/PropertiesOAIServer.java new file mode 100644 index 0000000..141258d --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/PropertiesOAIServer.java @@ -0,0 +1,144 @@ +package org.xbib.oai.server; + +import org.xbib.oai.OAISession; +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.getrecord.GetRecordServerRequest; +import org.xbib.oai.server.getrecord.GetRecordServerResponse; +import org.xbib.oai.server.identify.IdentifyServerRequest; +import org.xbib.oai.server.identify.IdentifyServerResponse; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerRequest; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerResponse; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerRequest; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerResponse; +import org.xbib.oai.server.listrecords.ListRecordsServerRequest; +import org.xbib.oai.server.listrecords.ListRecordsServerResponse; +import org.xbib.oai.server.listsets.ListSetsServerRequest; +import org.xbib.oai.server.listsets.ListSetsServerResponse; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Date; +import java.util.Properties; + +/** + * + */ +public class PropertiesOAIServer implements OAIServer { + + public static final String ADAPTER_URI = "uri"; + + public static final String STYLESHEET = "stylesheet"; + + public static final String REPOSITORY_NAME = "identify.repositoryName"; + + public static final String BASE_URL = "identify.baseURL"; + + public static final String PROTOCOL_VERSION = "identify.protocolVersion"; + + public static final String ADMIN_EMAIL = "identify.adminEmail"; + + public static final String EARLIEST_DATESTAMP = "identify.earliestDatestamp"; + + public static final String DELETED_RECORD = "identify.deletedRecord"; + + public static final String GRANULARITY = "identify.granularity"; + + private Properties properties; + + public PropertiesOAIServer(Properties properties) { + this.properties = properties; + } + + @Override + public URL getURL() { + try { + return new URL(properties.getProperty(ADAPTER_URI).trim()); + } catch (MalformedURLException e) { + // + } + return null; + } + + public String getStylesheet() { + return properties.getProperty(STYLESHEET); + } + + @Override + public String getRepositoryName() { + return properties.getProperty(REPOSITORY_NAME); + } + + @Override + public URL getBaseURL() { + try { + return new URL(properties.getProperty(BASE_URL)); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public String getProtocolVersion() { + return properties.getProperty(PROTOCOL_VERSION); + } + + @Override + public String getAdminEmail() { + return properties.getProperty(ADMIN_EMAIL); + } + + @Override + public String getEarliestDatestamp() { + return properties.getProperty(EARLIEST_DATESTAMP); + } + + @Override + public String getDeletedRecord() { + return properties.getProperty(DELETED_RECORD); + } + + @Override + public String getGranularity() { + return properties.getProperty(GRANULARITY); + } + + @Override + public OAISession newSession() { + return null; + } + + @Override + public Date getLastModified() { + return null; + } + + @Override + public void identify(IdentifyServerRequest request, IdentifyServerResponse response) + throws OAIException { + } + + @Override + public void listMetadataFormats(ListMetadataFormatsServerRequest request, ListMetadataFormatsServerResponse response) + throws OAIException { + } + + @Override + public void listSets(ListSetsServerRequest request, ListSetsServerResponse response) + throws OAIException { + } + + @Override + public void listIdentifiers(ListIdentifiersServerRequest request, ListIdentifiersServerResponse response) + throws OAIException { + } + + @Override + public void listRecords(ListRecordsServerRequest request, ListRecordsServerResponse response) + throws OAIException { + } + + @Override + public void getRecord(GetRecordServerRequest request, GetRecordServerResponse response) + throws OAIException { + } +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/ServerOAIRequest.java b/oai-server/src/main/java/org/xbib/oai/server/ServerOAIRequest.java new file mode 100644 index 0000000..84a1228 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/ServerOAIRequest.java @@ -0,0 +1,108 @@ +package org.xbib.oai.server; + +import org.xbib.oai.OAIConstants; +import org.xbib.oai.OAIRequest; +import org.xbib.oai.util.ResumptionToken; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public abstract class ServerOAIRequest implements OAIRequest { + + private String path; + + private Map parameters; + + private ResumptionToken token; + + private String set; + + private String metadataPrefix; + + private Instant from; + + private Instant until; + + private boolean retry; + + protected ServerOAIRequest() { + this.parameters = new HashMap<>(); + } + + @Override + public void setSet(String set) { + this.set = set; + parameters.put(OAIConstants.SET_PARAMETER, set); + } + + public String getSet() { + return set; + } + + @Override + public void setMetadataPrefix(String prefix) { + this.metadataPrefix = prefix; + parameters.put(OAIConstants.METADATA_PREFIX_PARAMETER, prefix); + } + + public String getMetadataPrefix() { + return metadataPrefix; + } + + @Override + public void setFrom(Instant from) { + this.from = from; + parameters.put(OAIConstants.FROM_PARAMETER, from.toString()); + } + + public Instant getFrom() { + return from; + } + + @Override + public void setUntil(Instant until) { + this.until = until; + parameters.put(OAIConstants.UNTIL_PARAMETER, until.toString()); + } + + public Instant getUntil() { + return until; + } + + @Override + public void setResumptionToken(ResumptionToken token) { + this.token = token; + if (token != null) { + parameters.put(OAIConstants.RESUMPTION_TOKEN_PARAMETER, token.toString()); + } + } + + @Override + public ResumptionToken getResumptionToken() { + return token; + } + + public void setRetry(boolean retry) { + this.retry = retry; + } + + public boolean isRetry() { + return retry; + } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + public Map getParameterMap() { + return parameters; + } +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/ServerOAIResponse.java b/oai-server/src/main/java/org/xbib/oai/server/ServerOAIResponse.java new file mode 100644 index 0000000..3a3cbe0 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/ServerOAIResponse.java @@ -0,0 +1,36 @@ +package org.xbib.oai.server; + +import org.xbib.oai.OAIResponse; + +import java.io.IOException; +import java.io.Writer; +import javax.xml.stream.util.XMLEventConsumer; + +/** + * Default OAI response. + */ +public class ServerOAIResponse implements OAIResponse { + + private String format; + + private XMLEventConsumer consumer; + + public String getOutputFormat() { + return format; + } + + @Override + public void to(Writer writer) throws IOException { + } + + + public ServerOAIResponse setConsumer(XMLEventConsumer consumer) { + this.consumer = consumer; + return this; + } + + public XMLEventConsumer getConsumer() { + return consumer; + } + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerRequest.java new file mode 100644 index 0000000..3c06305 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerRequest.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.getrecord; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class GetRecordServerRequest extends ServerOAIRequest { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerResponse.java new file mode 100644 index 0000000..a22105e --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/getrecord/GetRecordServerResponse.java @@ -0,0 +1,9 @@ +package org.xbib.oai.server.getrecord; + +import org.xbib.oai.server.ServerOAIResponse; + +/** + * + */ +public class GetRecordServerResponse extends ServerOAIResponse { +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/getrecord/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/getrecord/package-info.java new file mode 100644 index 0000000..ff75d21 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/getrecord/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI get record. + */ +package org.xbib.oai.server.getrecord; diff --git a/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerRequest.java new file mode 100644 index 0000000..918b259 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerRequest.java @@ -0,0 +1,9 @@ +package org.xbib.oai.server.identify; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class IdentifyServerRequest extends ServerOAIRequest { +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerResponse.java new file mode 100644 index 0000000..b157bf3 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/identify/IdentifyServerResponse.java @@ -0,0 +1,101 @@ +package org.xbib.oai.server.identify; + +import org.xbib.oai.server.ServerOAIResponse; + +import java.io.IOException; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * + */ +public class IdentifyServerResponse extends ServerOAIResponse { + + private String repositoryName; + + private URL baseURL; + + private String protocolVersion; + + private List adminEmails = new ArrayList<>(); + + private Date earliestDatestamp; + + private String deletedRecord; + + private String granularity; + + private String compression; + + @Override + public void to(Writer writer) throws IOException { + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getRepositoryName() { + return repositoryName; + } + + public void setBaseURL(URL url) { + this.baseURL = url; + } + + public URL getBaseURL() { + return baseURL; + } + + public void setProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public void addAdminEmail(String email) { + adminEmails.add(email); + } + + public List getAdminEmails() { + return adminEmails; + } + + public void setEarliestDatestamp(Date earliestDatestamp) { + this.earliestDatestamp = earliestDatestamp; + } + + public Date getEarliestDatestamp() { + return earliestDatestamp; + } + + public void setDeletedRecord(String deletedRecord) { + this.deletedRecord = deletedRecord; + } + + public String getDeleteRecord() { + return deletedRecord; + } + + public void setGranularity(String granularity) { + this.granularity = granularity; + } + + public String getGranularity() { + return granularity; + } + + public void setCompression(String compression) { + this.compression = compression; + } + + public String getCompression() { + return compression; + } + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/identify/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/identify/package-info.java new file mode 100644 index 0000000..55925e2 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/identify/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI identify verb. + */ +package org.xbib.oai.server.identify; diff --git a/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerRequest.java new file mode 100644 index 0000000..d338bf8 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerRequest.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listidentifiers; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class ListIdentifiersServerRequest extends ServerOAIRequest { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerResponse.java new file mode 100644 index 0000000..037bc6e --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/ListIdentifiersServerResponse.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listidentifiers; + +import org.xbib.oai.server.ServerOAIResponse; + +/** + * + */ +public class ListIdentifiersServerResponse extends ServerOAIResponse { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/package-info.java new file mode 100644 index 0000000..707a72f --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listidentifiers/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list identifiers verb. + */ +package org.xbib.oai.server.listidentifiers; diff --git a/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerRequest.java new file mode 100644 index 0000000..b6db919 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerRequest.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listmetadataformats; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class ListMetadataFormatsServerRequest extends ServerOAIRequest { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerResponse.java new file mode 100644 index 0000000..50ed930 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/ListMetadataFormatsServerResponse.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listmetadataformats; + +import org.xbib.oai.server.ServerOAIResponse; + +/** + * + */ +public class ListMetadataFormatsServerResponse extends ServerOAIResponse { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/package-info.java new file mode 100644 index 0000000..6411372 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listmetadataformats/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list metadata formats verb. + */ +package org.xbib.oai.server.listmetadataformats; diff --git a/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerRequest.java new file mode 100644 index 0000000..5b1248f --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerRequest.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listrecords; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class ListRecordsServerRequest extends ServerOAIRequest { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerResponse.java new file mode 100644 index 0000000..ed461e3 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listrecords/ListRecordsServerResponse.java @@ -0,0 +1,58 @@ +package org.xbib.oai.server.listrecords; + +import org.xbib.oai.server.ServerOAIResponse; + +import java.io.IOException; +import java.io.Writer; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + */ +public class ListRecordsServerResponse extends ServerOAIResponse { + + private static final Logger logger = Logger.getLogger(ListRecordsServerResponse.class.getName()); + + private String error; + + private Date date; + + private long expire; + + public void setError(String error) { + this.error = error; + } + + public String getError() { + return error; + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + public void setExpire(long expire) { + this.expire = expire; + } + + @Override + public void to(Writer writer) throws IOException { + try { + if (this.expire > 0L) { + logger.log(Level.INFO, "waiting for {} seconds (retry-after)", expire); + Thread.sleep(1000 * expire); + this.expire = 0L; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.log(Level.WARNING, "interrupted"); + } + } + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listrecords/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/listrecords/package-info.java new file mode 100644 index 0000000..afa1f27 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listrecords/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list records verb. + */ +package org.xbib.oai.server.listrecords; diff --git a/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerRequest.java b/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerRequest.java new file mode 100644 index 0000000..6e99d10 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerRequest.java @@ -0,0 +1,10 @@ +package org.xbib.oai.server.listsets; + +import org.xbib.oai.server.ServerOAIRequest; + +/** + * + */ +public class ListSetsServerRequest extends ServerOAIRequest { + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerResponse.java b/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerResponse.java new file mode 100644 index 0000000..c85aff8 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listsets/ListSetsServerResponse.java @@ -0,0 +1,9 @@ +package org.xbib.oai.server.listsets; + +import org.xbib.oai.server.ServerOAIResponse; + +/** + * + */ +public class ListSetsServerResponse extends ServerOAIResponse { +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/listsets/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/listsets/package-info.java new file mode 100644 index 0000000..87af901 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/listsets/package-info.java @@ -0,0 +1,4 @@ +/** + * OAI list sets verb. + */ +package org.xbib.oai.server.listsets; diff --git a/oai-server/src/main/java/org/xbib/oai/server/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/package-info.java new file mode 100644 index 0000000..4ca983e --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI server. + */ +package org.xbib.oai.server; diff --git a/oai-server/src/main/java/org/xbib/oai/server/verb/AbstractVerb.java b/oai-server/src/main/java/org/xbib/oai/server/verb/AbstractVerb.java new file mode 100644 index 0000000..3969947 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/verb/AbstractVerb.java @@ -0,0 +1,112 @@ +package org.xbib.oai.server.verb; + +import org.xbib.oai.OAIConstants; +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.OAIServer; +import org.xbib.oai.server.ServerOAIRequest; +import org.xbib.oai.server.ServerOAIResponse; + +import java.io.IOException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.TimeZone; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLStreamException; + +/** + * + */ +public abstract class AbstractVerb { + + private static final XMLEventFactory eventFactory = XMLEventFactory.newInstance(); + + protected final ServerOAIRequest request; + + protected final ServerOAIResponse response; + + private static final String NS_URI = "http://www.w3.org/2001/XMLSchema-instance"; + + private static final String NS_PREFIX = "xsi"; + + public AbstractVerb(ServerOAIRequest request, ServerOAIResponse response) { + this.request = request; + this.response = response; + } + + public abstract void execute(OAIServer adapter) throws OAIException; + + protected void beginDocument() throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartDocument()); + } + + protected void endDocument() throws XMLStreamException, IOException { + response.getConsumer().add(eventFactory.createEndDocument()); + } + + protected void beginElement(String name) throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartElement(toQName(OAIConstants.NS_URI, name), null, null)); + } + + protected void endElement(String name) throws XMLStreamException { + response.getConsumer().add(eventFactory.createEndElement(toQName(OAIConstants.NS_URI, name), null)); + } + + protected void element(String name, String value) throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartElement(toQName(OAIConstants.NS_URI, name), null, null)); + response.getConsumer().add(eventFactory.createCharacters(value)); + response.getConsumer().add(eventFactory.createEndElement(toQName(OAIConstants.NS_URI, name), null)); + } + + protected void element(String name, Date value) throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartElement(toQName(OAIConstants.NS_URI, name), null, null)); + response.getConsumer().add(eventFactory.createCharacters(formatDate(value))); + response.getConsumer().add(eventFactory.createEndElement(toQName(OAIConstants.NS_URI, name), null)); + } + + protected void beginOAIPMH(URL baseURL) throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartElement(toQName(OAIConstants.NS_URI, "OAI-PMH"), null, null)); + response.getConsumer().add(eventFactory.createNamespace(OAIConstants.NS_URI)); + response.getConsumer().add(eventFactory.createNamespace(NS_PREFIX, NS_URI)); + response.getConsumer().add(eventFactory.createAttribute(NS_PREFIX, NS_URI, + "schemaLocation", "http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd")); + element("responseDate", new Date()); + request(request.getParameterMap(), baseURL); + } + + protected void endOAIPMH() throws XMLStreamException { + response.getConsumer().add(eventFactory.createEndElement(toQName(OAIConstants.NS_URI, "OAI-PMH"), null)); + } + + protected void request(Map attrs, URL baseURL) throws XMLStreamException { + response.getConsumer().add(eventFactory.createStartElement(toQName(OAIConstants.NS_URI, OAIConstants.REQUEST), + null, null)); + for (Map.Entry me : attrs.entrySet()) { + response.getConsumer().add(eventFactory.createAttribute(me.getKey(), me.getValue())); + } + response.getConsumer().add(eventFactory.createCharacters(baseURL.toExternalForm())); + response.getConsumer().add(eventFactory.createEndElement(toQName(OAIConstants.NS_URI, OAIConstants.REQUEST), + null)); + } + + private QName toQName(String namespaceUri, String qname) { + int i = qname.indexOf(':'); + if (i == -1) { + return new QName(namespaceUri, qname); + } else { + String prefix = qname.substring(0, i); + String localPart = qname.substring(i + 1); + return new QName(namespaceUri, localPart, prefix); + } + } + + private final TimeZone tz = TimeZone.getTimeZone("GMT"); + + private String formatDate(Date date) { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + format.setTimeZone(tz); + return format.format(date); + } +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/verb/Identify.java b/oai-server/src/main/java/org/xbib/oai/server/verb/Identify.java new file mode 100644 index 0000000..e837f81 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/verb/Identify.java @@ -0,0 +1,31 @@ +package org.xbib.oai.server.verb; + +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.OAIServer; +import org.xbib.oai.server.identify.IdentifyServerRequest; +import org.xbib.oai.server.identify.IdentifyServerResponse; + +/** + * + */ +public class Identify extends AbstractVerb { + + public Identify(IdentifyServerRequest request, IdentifyServerResponse response) { + super(request, response); + } + + @Override + public void execute(OAIServer adapter) throws OAIException { + try { + beginDocument(); + beginOAIPMH(adapter.getBaseURL()); + beginElement("Identify"); + endElement("Identify"); + endOAIPMH(); + endDocument(); + } catch (Exception e) { + throw new OAIException(e.getMessage(), e); + } + } + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/verb/ListMetadataFormats.java b/oai-server/src/main/java/org/xbib/oai/server/verb/ListMetadataFormats.java new file mode 100644 index 0000000..75a30f8 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/verb/ListMetadataFormats.java @@ -0,0 +1,31 @@ +package org.xbib.oai.server.verb; + +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.OAIServer; +import org.xbib.oai.server.ServerOAIRequest; +import org.xbib.oai.server.ServerOAIResponse; + +/** + * + */ +public class ListMetadataFormats extends AbstractVerb { + + public ListMetadataFormats(ServerOAIRequest request, ServerOAIResponse response) { + super(request, response); + } + + @Override + public void execute(OAIServer adapter) throws OAIException { + try { + beginDocument(); + beginOAIPMH(adapter.getBaseURL()); + beginElement("ListMetadataFormats"); + endElement("ListMetadataFormats"); + endOAIPMH(); + endDocument(); + } catch (Exception e) { + throw new OAIException(e.getMessage(), e); + } + } + +} diff --git a/oai-server/src/main/java/org/xbib/oai/server/verb/package-info.java b/oai-server/src/main/java/org/xbib/oai/server/verb/package-info.java new file mode 100644 index 0000000..aa99c77 --- /dev/null +++ b/oai-server/src/main/java/org/xbib/oai/server/verb/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI server verbs. + */ +package org.xbib.oai.server.verb; diff --git a/oai-server/src/test/java/org/xbib/oai/server/SimpleServer.java b/oai-server/src/test/java/org/xbib/oai/server/SimpleServer.java new file mode 100644 index 0000000..481eec3 --- /dev/null +++ b/oai-server/src/test/java/org/xbib/oai/server/SimpleServer.java @@ -0,0 +1,116 @@ +package org.xbib.oai.server; + +import org.xbib.oai.OAISession; +import org.xbib.oai.exceptions.OAIException; +import org.xbib.oai.server.getrecord.GetRecordServerRequest; +import org.xbib.oai.server.getrecord.GetRecordServerResponse; +import org.xbib.oai.server.identify.IdentifyServerRequest; +import org.xbib.oai.server.identify.IdentifyServerResponse; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerRequest; +import org.xbib.oai.server.listidentifiers.ListIdentifiersServerResponse; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerRequest; +import org.xbib.oai.server.listmetadataformats.ListMetadataFormatsServerResponse; +import org.xbib.oai.server.listrecords.ListRecordsServerRequest; +import org.xbib.oai.server.listrecords.ListRecordsServerResponse; +import org.xbib.oai.server.listsets.ListSetsServerRequest; +import org.xbib.oai.server.listsets.ListSetsServerResponse; +import org.xbib.oai.server.verb.Identify; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Date; + +/** + * + */ +public class SimpleServer implements OAIServer { + + @Override + public void identify(IdentifyServerRequest request, IdentifyServerResponse response) + throws OAIException { + new Identify(request, response).execute(this); + } + + @Override + public void listMetadataFormats(ListMetadataFormatsServerRequest request, ListMetadataFormatsServerResponse response) + throws OAIException { + } + + @Override + public void listSets(ListSetsServerRequest request, ListSetsServerResponse response) + throws OAIException { + } + + @Override + public void listIdentifiers(ListIdentifiersServerRequest request, ListIdentifiersServerResponse response) + throws OAIException { + } + + @Override + public void listRecords(ListRecordsServerRequest request, ListRecordsServerResponse response) + throws OAIException { + } + + @Override + public void getRecord(GetRecordServerRequest request, GetRecordServerResponse response) + throws OAIException { + } + + @Override + public URL getURL() { + try { + return new URL("http://localhost:8080/oai"); + } catch (MalformedURLException e) { + // + } + return null; + } + + @Override + public OAISession newSession() throws URISyntaxException { + //return new DefaultOAIClient().setURL(getURL()); + return null; + } + + @Override + public Date getLastModified() { + return new Date(); + } + + @Override + public String getRepositoryName() { + return "Test Repository Name"; + } + + @Override + public URL getBaseURL() { + return getURL(); + } + + @Override + public String getProtocolVersion() { + return "2.0"; + } + + @Override + public String getAdminEmail() { + return "joergprante@gmail.com"; + } + + @Override + public String getEarliestDatestamp() { + return "2012-01-01T00:00:00Z"; + } + + @Override + public String getDeletedRecord() { + return "no"; + } + + @Override + public String getGranularity() { + return "YYYY-MM-DDThh:mm:ssZ"; + } + +} diff --git a/oai-server/src/test/java/org/xbib/oai/server/SimpleServiceTest.java b/oai-server/src/test/java/org/xbib/oai/server/SimpleServiceTest.java new file mode 100644 index 0000000..5834526 --- /dev/null +++ b/oai-server/src/test/java/org/xbib/oai/server/SimpleServiceTest.java @@ -0,0 +1,27 @@ +package org.xbib.oai.server; + +import org.junit.Test; +import org.xbib.oai.server.identify.IdentifyServerRequest; +import org.xbib.oai.server.identify.IdentifyServerResponse; + +import java.io.StringWriter; +import javax.xml.stream.XMLOutputFactory; + +/** + * + */ +public class SimpleServiceTest { + + @Test + public void testIdentifyService() throws Exception { + OAIServer service = OAIServiceFactory.getDefaultService(); + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + IdentifyServerRequest request = new IdentifyServerRequest(); + IdentifyServerResponse response = new IdentifyServerResponse(); + response.setConsumer(factory.createXMLEventWriter(sw)); + service.identify(request, response); + response.to(sw); + } + +} diff --git a/oai-server/src/test/java/org/xbib/oai/server/package-info.java b/oai-server/src/test/java/org/xbib/oai/server/package-info.java new file mode 100644 index 0000000..4ca983e --- /dev/null +++ b/oai-server/src/test/java/org/xbib/oai/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for OAI server. + */ +package org.xbib.oai.server; diff --git a/oai-server/src/test/resources/META-INF/services/org.xbib.oai.server.OAIServer b/oai-server/src/test/resources/META-INF/services/org.xbib.oai.server.OAIServer new file mode 100644 index 0000000..e1d21db --- /dev/null +++ b/oai-server/src/test/resources/META-INF/services/org.xbib.oai.server.OAIServer @@ -0,0 +1 @@ +org.xbib.oai.server.SimpleServer diff --git a/oai-server/src/test/resources/log4j2.xml b/oai-server/src/test/resources/log4j2.xml new file mode 100644 index 0000000..b175dfc --- /dev/null +++ b/oai-server/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/oai-server/src/test/resources/org/xbib/oai/server/test.properties b/oai-server/src/test/resources/org/xbib/oai/server/test.properties new file mode 100644 index 0000000..5233ce2 --- /dev/null +++ b/oai-server/src/test/resources/org/xbib/oai/server/test.properties @@ -0,0 +1,7 @@ +identify.repositoryName=Test Repository Name +identify.baseURL=http://localhost:8080/oai +identify.protocolVersion=2.0 +identify.adminEmail=joergprante@gmail.com +identify.earliestDatestamp=2012-01-01T00:00:00Z +identify.deletedRecord=no +identify.granularity=YYYY-MM-DDThh:mm:ssZ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..75b407e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'oai' +include 'oai-common' +include 'oai-client' +include 'oai-server'