diff --git a/.gitignore b/.gitignore
index 3df9a13..15010ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,7 +82,8 @@ local.properties
.scala_dependencies
.worksheet
-# Uncomment this line if you wish to ignore the project description file.
-# Typically, this file would be tracked if it contains build/dependency configurations:
-#.project
-
+# my exclusions
+.project
+.classpath
+.java-version
+target/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c4c5cb6
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,112 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+ eu.dnetlib
+ dnet-oai-server
+ 0.0.1-SNAPSHOT
+ dnet-oai-server
+ Simple OAI-PMH server
+
+ 17
+
+
+
+
+ org.dom4j
+ dom4j
+ 2.1.4
+
+
+
+ jaxen
+ jaxen
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+ commons-codec
+ commons-codec
+
+
+
+ commons-io
+ commons-io
+ 2.16.1
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-json
+
+
+
+ com.fasterxml.jackson.module
+ jackson-module-jakarta-xmlbind-annotations
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-help-plugin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..8380a69
Binary files /dev/null and b/src/.DS_Store differ
diff --git a/src/main/.DS_Store b/src/main/.DS_Store
new file mode 100644
index 0000000..a06ac0f
Binary files /dev/null and b/src/main/.DS_Store differ
diff --git a/src/main/java/eu/dnetlib/apps/oai/OaiServerApplication.java b/src/main/java/eu/dnetlib/apps/oai/OaiServerApplication.java
new file mode 100644
index 0000000..d849d8f
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/OaiServerApplication.java
@@ -0,0 +1,13 @@
+package eu.dnetlib.apps.oai;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class OaiServerApplication {
+
+ public static void main(final String[] args) {
+ SpringApplication.run(OaiServerApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/OaiServerConf.java b/src/main/java/eu/dnetlib/apps/oai/OaiServerConf.java
new file mode 100644
index 0000000..2a8e7eb
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/OaiServerConf.java
@@ -0,0 +1,42 @@
+package eu.dnetlib.apps.oai;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
+
+@Component
+@Configuration
+@ConfigurationProperties(prefix = "oai.server")
+public class OaiServerConf {
+
+ private String baseUrl;
+
+ private String repositoryName;
+
+ private String adminEmail;
+
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(final String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getRepositoryName() {
+ return repositoryName;
+ }
+
+ public void setRepositoryName(final String repositoryName) {
+ this.repositoryName = repositoryName;
+ }
+
+ public String getAdminEmail() {
+ return adminEmail;
+ }
+
+ public void setAdminEmail(final String adminEmail) {
+ this.adminEmail = adminEmail;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/OaiServerController.java b/src/main/java/eu/dnetlib/apps/oai/OaiServerController.java
new file mode 100644
index 0000000..719fc72
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/OaiServerController.java
@@ -0,0 +1,282 @@
+package eu.dnetlib.apps.oai;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import eu.dnetlib.apps.oai.domain.OaiError;
+import eu.dnetlib.apps.oai.domain.OaiMetadataFormat;
+import eu.dnetlib.apps.oai.domain.OaiPage;
+import eu.dnetlib.apps.oai.domain.OaiRecord;
+import eu.dnetlib.apps.oai.domain.OaiSet;
+import eu.dnetlib.apps.oai.domain.OaiVerb;
+import eu.dnetlib.apps.oai.utils.DateUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@Controller
+public class OaiServerController {
+
+ private static final String DEFAULT_CONTENT_TYPE = "text/xml;charset=utf-8";
+
+ @Autowired
+ private OaiService oaiService;
+
+ @Autowired
+ private OaiServerConf oaiConf;
+
+ private static final Log log = LogFactory.getLog(OaiServerController.class);
+
+ @RequestMapping("/oai")
+ public void oaiCall(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ response.setContentType(OaiServerController.DEFAULT_CONTENT_TYPE);
+
+ final Map params = cleanParameters(request.getParameterMap());
+
+ try (final OutputStream out = response.getOutputStream()) {
+ IOUtils.write(oaiResponse(params), out, StandardCharsets.UTF_8);
+ }
+
+ }
+
+ private String oaiResponse(final Map params) {
+ if (params == null) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ final String verb = params.remove("verb");
+
+ return switch (OaiVerb.validate(verb)) {
+ case IDENTIFY -> oaiIdentify(params);
+ case LIST_METADATA_FORMATS -> oaiListMetadataFormats(params);
+ case LIST_SETS -> oaiListSets(params);
+ case GET_RECORD -> oaiGetRecord(params);
+ case LIST_IDENTIFIERS -> oaiListIdentifiers(params);
+ case LIST_RECORDS -> oaiListRecords(params);
+ default -> prepareErrorResponseXml(OaiError.badVerb);
+ };
+ }
+
+ private String oaiIdentify(final Map params) {
+ if (!params.isEmpty()) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ final Document doc = genericOaiResponse(OaiVerb.IDENTIFY.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.IDENTIFY.getVerb());
+
+ dataNode.addElement("baseURL").setText(oaiConf.getBaseUrl());
+ dataNode.addElement("repositoryName").setText(oaiConf.getRepositoryName());
+ dataNode.addElement("protocolVersion").setText("2.0");
+ dataNode.addElement("adminEmail").setText(oaiConf.getAdminEmail());
+ dataNode.addElement("earliestDatestamp").setText("1900-01-01T00:00:00Z");
+ dataNode.addElement("deletedRecord").setText("transient");
+ dataNode.addElement("granularity").setText("YYYY-MM-DDThh:mm:ssZ");
+
+ return doc.asXML();
+ }
+
+ private String oaiListMetadataFormats(final Map params) {
+ final String id = params.remove("identifier");
+ if (!params.isEmpty()) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ final Document doc = genericOaiResponse(OaiVerb.LIST_METADATA_FORMATS.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.LIST_METADATA_FORMATS.getVerb());
+
+ final List formats =
+ StringUtils.isBlank(id) ? this.oaiService.listMetadataFormats(id) : this.oaiService.listMetadataFormats();
+
+ for (final OaiMetadataFormat oaiFormat : formats) {
+ final Element formatNode = dataNode.addElement("metadataFormat");
+ formatNode.addElement("metadataPrefix").setText(oaiFormat.getMetadataPrefix());
+ formatNode.addElement("schema").setText(oaiFormat.getMetadataSchema());
+ formatNode.addElement("metadataNamespace").setText(oaiFormat.getMetadataNamespace());
+ }
+ return doc.asXML();
+ }
+
+ private String oaiListSets(final Map params) {
+ if (!params.isEmpty()) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ final Document doc = genericOaiResponse(OaiVerb.LIST_SETS.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.LIST_SETS.getVerb());
+
+ for (final OaiSet oaiSet : this.oaiService.listSets()) {
+ final Element setNode = dataNode.addElement("set");
+ setNode.addElement("setSpec").setText(oaiSet.getSetSpec());
+ setNode.addElement("setName").setText(oaiSet.getSetName());
+ setNode.addElement("setDescription").setText(oaiSet.getDescription());
+ }
+ return doc.asXML();
+
+ }
+
+ private String oaiGetRecord(final Map params) {
+ final String prefix = params.remove("metadataPrefix");
+ final String identifier = params.remove("identifier");
+ if (!params.isEmpty() || StringUtils.isAnyBlank(prefix, identifier)) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ final OaiRecord record = this.oaiService.getRecord(identifier, prefix);
+ if (record == null) { return prepareErrorResponseXml(OaiError.idDoesNotExist); }
+
+ final Document doc = genericOaiResponse(OaiVerb.GET_RECORD.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.GET_RECORD.getVerb());
+
+ insertSingleRecord(dataNode, record);
+
+ return doc.asXML();
+ }
+
+ private String oaiListRecords(final Map params) {
+ final OaiPage page;
+ if (params.containsKey("resumptionToken")) {
+ final String resumptionToken = params.remove("resumptionToken");
+ if (!params.isEmpty()) { return prepareErrorResponseXml(OaiError.badArgument); }
+ page = this.oaiService.listRecords(resumptionToken);
+ } else {
+
+ final String metadataPrefix = params.remove("metadataPrefix");
+ final String from = params.remove("from");
+ final String until = params.remove("until");
+ final String set = params.remove("set");
+
+ if (!StringUtils.isNotBlank(metadataPrefix) || !this.oaiService.verifySet(set)) { return prepareErrorResponseXml(OaiError.badArgument); }
+ page = this.oaiService.listRecords(metadataPrefix, set, from, until);
+ }
+
+ final Document doc = genericOaiResponse(OaiVerb.LIST_RECORDS.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.LIST_RECORDS.getVerb());
+
+ page.getList().forEach(record -> insertSingleRecord(dataNode, record));
+
+ insertResumptionToken(dataNode, page);
+
+ return doc.asXML();
+ }
+
+ private void insertSingleRecord(final Element parentNode, final OaiRecord record) {
+ final Element recordNode = parentNode.addElement("record");
+ insertRecordHeader(recordNode, record);
+ try {
+ final Document doc2 = DocumentHelper.parseText(record.getBody());
+ recordNode.addElement("metadata").add(doc2.getRootElement());
+ } catch (final DocumentException e) {
+ log.warn("Error parsing record: " + record.getBody());
+ }
+ }
+
+ private String oaiListIdentifiers(final Map params) {
+
+ final OaiPage page;
+
+ if (params.containsKey("resumptionToken")) {
+ final String resumptionToken = params.remove("resumptionToken");
+ if (!params.isEmpty()) { return prepareErrorResponseXml(OaiError.badArgument); }
+ page = this.oaiService.listRecords(resumptionToken);
+ } else {
+ final String metadataPrefix = params.remove("metadataPrefix");
+ final String from = params.remove("from");
+ final String until = params.remove("until");
+ final String set = params.remove("set");
+
+ if (!StringUtils.isNotBlank(metadataPrefix) || !this.oaiService.verifySet(set)) { return prepareErrorResponseXml(OaiError.badArgument); }
+
+ page = this.oaiService.listRecords(metadataPrefix, set, from, until);
+ }
+
+ final Document doc = genericOaiResponse(OaiVerb.LIST_IDENTIFIERS.getVerb());
+ final Element dataNode = doc.getRootElement().addElement(OaiVerb.LIST_IDENTIFIERS.getVerb());
+
+ page.getList().forEach(r -> insertRecordHeader(dataNode, r));
+
+ insertResumptionToken(dataNode, page);
+
+ return doc.asXML();
+ }
+
+ private void insertRecordHeader(final Element parentNode, final OaiRecord r) {
+ final Element headerNode = parentNode.addElement("header");
+ headerNode.addElement("identifier").setText(r.getId());
+ headerNode.addElement("datestamp").setText(DateUtils.calculate_ISO8601(r.getDate()));
+ if (StringUtils.isNotBlank(r.getOaiSet())) {
+ headerNode.addElement("setSpec").setText(r.getOaiSet());
+ }
+ }
+
+ private void insertResumptionToken(final Element parentNode, final OaiPage page) {
+ if (StringUtils.isNotBlank(page.getResumptionToken())) {
+ final Element tokenNode = parentNode.addElement("resumptionToken");
+ tokenNode.addAttribute("completeListSize", Long.toString(page.getTotal()));
+ tokenNode.addAttribute("cursor", Long.toString(page.getCursor()));
+ tokenNode.setText(page.getResumptionToken());
+ }
+ }
+
+ private Map cleanParameters(final Map, ?> startParams) {
+ final HashMap params = new HashMap<>();
+ final Iterator> iter = startParams.entrySet().iterator();
+ while (iter.hasNext()) {
+ final Entry, ?> entry = (Entry, ?>) iter.next();
+ final String key = entry.getKey().toString();
+ final String[] arr = (String[]) entry.getValue();
+ if (arr.length == 0) { return null; }
+ final String value = arr[0];
+ if ("verb".equals(key)) {
+ params.put("verb", value);
+ } else if ("from".equals(key)) {
+ params.put("from", value);
+ } else if ("until".equals(key)) {
+ params.put("until", value);
+ } else if ("metadataPrefix".equals(key)) {
+ params.put("metadataPrefix", value);
+ } else if ("identifier".equals(key)) {
+ params.put("identifier", value);
+ } else if ("set".equals(key)) {
+ params.put("set", value);
+ } else if ("resumptionToken".equals(key)) {
+ params.put("resumptionToken", value);
+ } else {
+ return null;
+ }
+ }
+ return params;
+ }
+
+ private Document genericOaiResponse(final String verb) {
+ try (InputStream is = getClass().getResourceAsStream("/oai/oai_response.xml")) {
+ final Document doc = new SAXReader().read(is);
+ doc.selectSingleNode("//*[local-name() = 'responseDate']").setText(DateUtils.now_ISO8601());
+ doc.selectSingleNode("//*[local-name() = 'request']").setText(oaiConf.getBaseUrl());
+ if (StringUtils.isNotBlank(verb)) {
+ doc.selectSingleNode("//*[local-name() = 'request']/@verb").setText(verb);
+ }
+ return doc;
+ } catch (final DocumentException | IOException e) {
+ throw new RuntimeException("Error generataing oai response", e);
+ }
+ }
+
+ private String prepareErrorResponseXml(final OaiError error) {
+ final Document doc = genericOaiResponse(null);
+ final Element errorNode = doc.getRootElement().addElement("error");
+ errorNode.addAttribute("code", error.name());
+ errorNode.setText(error.getMessage());
+ return doc.asXML();
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/OaiService.java b/src/main/java/eu/dnetlib/apps/oai/OaiService.java
new file mode 100644
index 0000000..627529e
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/OaiService.java
@@ -0,0 +1,50 @@
+package eu.dnetlib.apps.oai;
+
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+
+import eu.dnetlib.apps.oai.domain.OaiMetadataFormat;
+import eu.dnetlib.apps.oai.domain.OaiPage;
+import eu.dnetlib.apps.oai.domain.OaiRecord;
+import eu.dnetlib.apps.oai.domain.OaiSet;
+
+@Service
+public class OaiService {
+
+ public boolean verifySet(final String set) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ public OaiPage listRecords(final String resumptionToken) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public OaiPage listRecords(final String metadataPrefix, final String set, final String from, final String until) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public List listMetadataFormats(final String id) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public List listMetadataFormats() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public OaiRecord getRecord(final String identifier, final String prefix) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public OaiSet[] listSets() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiError.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiError.java
new file mode 100644
index 0000000..6baf684
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiError.java
@@ -0,0 +1,26 @@
+package eu.dnetlib.apps.oai.domain;
+
+public enum OaiError {
+
+ badArgument(
+ "The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax."),
+ badVerb("Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated."),
+ cannotDisseminateFormat(
+ "The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository."),
+ idDoesNotExist("The value of the identifier argument is unknown or illegal in this repository."),
+ noMetadataFormats("There are no metadata formats available for the specified item."),
+ noSetHierarchy("The repository does not support sets."),
+ noRecordsMatch("The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list."),
+ badResumptionToken("The value of the resumptionToken argument is invalid or expired.");
+
+ private final String message;
+
+ OaiError(final String message) {
+ this.message = message;
+ }
+
+ public final String getMessage() {
+ return message;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiMetadataFormat.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiMetadataFormat.java
new file mode 100644
index 0000000..bfc4b85
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiMetadataFormat.java
@@ -0,0 +1,49 @@
+package eu.dnetlib.apps.oai.domain;
+
+import java.io.Serializable;
+
+public class OaiMetadataFormat implements Serializable {
+
+ private static final long serialVersionUID = -3225471556469204065L;
+
+ private String metadataPrefix;
+
+ private String metadataSchema;
+
+ private String metadataNamespace;
+
+ private String xslt;
+
+ public String getMetadataPrefix() {
+ return this.metadataPrefix;
+ }
+
+ public void setMetadataPrefix(final String metadataPrefix) {
+ this.metadataPrefix = metadataPrefix;
+ }
+
+ public String getMetadataSchema() {
+ return this.metadataSchema;
+ }
+
+ public void setMetadataSchema(final String metadataSchema) {
+ this.metadataSchema = metadataSchema;
+ }
+
+ public String getMetadataNamespace() {
+ return this.metadataNamespace;
+ }
+
+ public void setMetadataNamespace(final String metadataNamespace) {
+ this.metadataNamespace = metadataNamespace;
+ }
+
+ public String getXslt() {
+ return this.xslt;
+ }
+
+ public void setXslt(final String xslt) {
+ this.xslt = xslt;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiPage.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiPage.java
new file mode 100644
index 0000000..28f36f3
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiPage.java
@@ -0,0 +1,50 @@
+package eu.dnetlib.apps.oai.domain;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class OaiPage implements Serializable {
+
+ private static final long serialVersionUID = -7512951692582271344L;
+
+ private List list;
+
+ private long total;
+
+ private long cursor;
+
+ private String resumptionToken;
+
+ public List getList() {
+ return list;
+ }
+
+ public void setList(final List list) {
+ this.list = list;
+ }
+
+ public long getTotal() {
+ return total;
+ }
+
+ public void setTotal(final long total) {
+ this.total = total;
+ }
+
+ public long getCursor() {
+ return cursor;
+ }
+
+ public void setCursor(final long cursor) {
+ this.cursor = cursor;
+ }
+
+ public String getResumptionToken() {
+ return resumptionToken;
+ }
+
+ public void setResumptionToken(final String resumptionToken) {
+ this.resumptionToken = resumptionToken;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiPageRequest.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiPageRequest.java
new file mode 100644
index 0000000..e4c3ba0
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiPageRequest.java
@@ -0,0 +1,114 @@
+package eu.dnetlib.apps.oai.domain;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+
+import eu.dnetlib.apps.oai.utils.DateUtils;
+
+public class OaiPageRequest {
+
+ private static final String SEPARATOR = "@@@";
+
+ private String metadataPrefix;
+ private String set;
+ private LocalDate from;
+ private LocalDate until;
+ private int pageNumber;
+ private int pageSize;
+
+ private OaiPageRequest(final String metadataPrefix, final String set, final LocalDate from, final LocalDate until, final int pageNumber,
+ final int pageSize) {
+ super();
+ this.metadataPrefix = metadataPrefix;
+ this.set = set;
+ this.from = from;
+ this.until = until;
+ this.pageNumber = pageNumber;
+ this.pageSize = pageSize;
+ }
+
+ public static OaiPageRequest prepareRequest(final String metadataPrefix,
+ final String set,
+ final LocalDate from,
+ final LocalDate until,
+ final int pageNumber,
+ final int pageSize) {
+ return new OaiPageRequest(metadataPrefix, set, from, until, pageNumber, pageSize);
+ }
+
+ public static OaiPageRequest fromResumptionToken(final String resumptionToken) {
+
+ final String[] arr = StringUtils.split(new String(Base64.decodeBase64(resumptionToken)), SEPARATOR);
+
+ if (arr.length != 6) { throw new RuntimeException("Invalid token"); }
+ final String metadataPrefix = arr[0];
+ final String set = arr[1];
+ final LocalDate from = DateUtils.parseDate(arr[2]);
+ final LocalDate until = DateUtils.parseDate(arr[3]);
+ final int pageNumber = Integer.parseInt(arr[4]);
+ final int pageSize = Integer.parseInt(arr[5]);
+
+ return new OaiPageRequest(metadataPrefix, set, from, until, pageNumber, pageSize);
+ }
+
+ public String getMetadataPrefix() {
+ return metadataPrefix;
+ }
+
+ public void setMetadataPrefix(final String metadataPrefix) {
+ this.metadataPrefix = metadataPrefix;
+ }
+
+ public String getSet() {
+ return set;
+ }
+
+ public void setSet(final String set) {
+ this.set = set;
+ }
+
+ public LocalDate getFrom() {
+ return from;
+ }
+
+ public void setFrom(final LocalDate from) {
+ this.from = from;
+ }
+
+ public LocalDate getUntil() {
+ return until;
+ }
+
+ public void setUntil(final LocalDate until) {
+ this.until = until;
+ }
+
+ public String nextResumptionToken() {
+ final List list = Arrays.asList(metadataPrefix, set, from.toString(), until.toString(), Integer.toString(pageNumber + 1), Integer
+ .toString(pageSize));
+
+ final String s = StringUtils.join(list, SEPARATOR);
+
+ return Base64.encodeBase64URLSafeString(s.getBytes());
+ }
+
+ public int getPageNumber() {
+ return pageNumber;
+ }
+
+ public void setPageNumber(final int pageNumber) {
+ this.pageNumber = pageNumber;
+ }
+
+ public int getPageSize() {
+ return pageSize;
+ }
+
+ public void setPageSize(final int pageSize) {
+ this.pageSize = pageSize;
+ }
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiRecord.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiRecord.java
new file mode 100644
index 0000000..fdcf95f
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiRecord.java
@@ -0,0 +1,59 @@
+package eu.dnetlib.apps.oai.domain;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+public class OaiRecord implements Serializable {
+
+ private static final long serialVersionUID = -8383104201424481929L;
+
+ private String id;
+
+ private String body;
+
+ private LocalDateTime date;
+
+ private String oaiSet;
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ public String getBody() {
+ return this.body;
+ }
+
+ public void setBody(final String body) {
+ this.body = body;
+ }
+
+ public LocalDateTime getDate() {
+ return this.date;
+ }
+
+ public void setDate(final LocalDateTime date) {
+ this.date = date;
+ }
+
+ public String getOaiSet() {
+ return this.oaiSet;
+ }
+
+ public void setOaiSet(final String oaiSet) {
+ this.oaiSet = oaiSet;
+ }
+
+ public List getSets() {
+ return Arrays.asList(this.oaiSet);
+ }
+
+ public boolean isDeleted() {
+ return false;
+ }
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiSet.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiSet.java
new file mode 100644
index 0000000..a61fea9
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiSet.java
@@ -0,0 +1,53 @@
+package eu.dnetlib.apps.oai.domain;
+
+import java.io.Serializable;
+
+public class OaiSet implements Serializable {
+
+ private static final long serialVersionUID = 1995486356252936048L;
+
+ private String setSpec;
+
+ private String setName;
+
+ private String description;
+
+ private String dsId;
+
+ public String getSetSpec() {
+ return this.setSpec;
+ }
+
+ public void setSetSpec(final String setSpec) {
+ this.setSpec = setSpec;
+ }
+
+ public String getSetName() {
+ return this.setName;
+ }
+
+ public void setSetName(final String setName) {
+ this.setName = setName;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public String getDsId() {
+ return this.dsId;
+ }
+
+ public void setDsId(final String dsId) {
+ this.dsId = dsId;
+ }
+
+ public static long getSerialversionuid() {
+ return serialVersionUID;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/domain/OaiVerb.java b/src/main/java/eu/dnetlib/apps/oai/domain/OaiVerb.java
new file mode 100644
index 0000000..ac4e2e5
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/domain/OaiVerb.java
@@ -0,0 +1,36 @@
+package eu.dnetlib.apps.oai.domain;
+
+import org.apache.commons.lang3.StringUtils;
+
+public enum OaiVerb {
+
+ IDENTIFY("Identify"),
+ LIST_IDENTIFIERS("ListIdentifiers"),
+ LIST_RECORDS("ListRecords"),
+ LIST_METADATA_FORMATS("ListMetadataFormats"),
+ LIST_SETS("ListSets"),
+ GET_RECORD("GetRecord"),
+ UNSUPPORTED_VERB("");
+
+ private final String verb;
+
+ public static OaiVerb validate(final String verb) {
+
+ if (StringUtils.isBlank(verb)) { return UNSUPPORTED_VERB; }
+
+ for (final OaiVerb v : OaiVerb.values()) {
+ if (v.getVerb().equalsIgnoreCase(verb)) { return v; }
+ }
+
+ return UNSUPPORTED_VERB;
+ }
+
+ private OaiVerb(final String verb) {
+ this.verb = verb;
+ }
+
+ public String getVerb() {
+ return verb;
+ }
+
+}
diff --git a/src/main/java/eu/dnetlib/apps/oai/utils/DateUtils.java b/src/main/java/eu/dnetlib/apps/oai/utils/DateUtils.java
new file mode 100644
index 0000000..3f2310c
--- /dev/null
+++ b/src/main/java/eu/dnetlib/apps/oai/utils/DateUtils.java
@@ -0,0 +1,39 @@
+package eu.dnetlib.apps.oai.utils;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class DateUtils {
+
+ private static final DateTimeFormatter DATEFORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.getDefault());
+
+ private static final DateTimeFormatter ISO8601FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
+
+ public static LocalDate parseDate(final String s) {
+ return LocalDate.parse(s, DATEFORMAT);
+ }
+
+ public static String calculate_ISO8601(final LocalDateTime time) {
+ final String result = time.format(ISO8601FORMAT);
+ return result.substring(0, result.length() - 2) + ":" + result.substring(result.length() - 2);
+ }
+
+ public static String calculate_ISO8601(final long l) {
+ return calculate_ISO8601(LocalDateTime.ofInstant(Instant.ofEpochMilli(l), TimeZone
+ .getDefault()
+ .toZoneId()));
+ }
+
+ public static long now() {
+ return Instant.now().toEpochMilli();
+ }
+
+ public static String now_ISO8601() {
+ return calculate_ISO8601(now());
+ }
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..2178965
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1,17 @@
+spring.application.name=dnet-oai-server
+
+oai.server.baseUrl = http://localhost:8080/oai
+oai.server.repositoryName = TEST repository
+oai.server.adminEmail = test@test
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/oai_server_test
+spring.datasource.username=
+spring.datasource.password=
+
+spring.jpa.hibernate.ddl-auto = validate
+spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
+spring.jpa.open-in-view=true
+spring.jpa.properties.hibernate.show_sql=false
+spring.jpa.properties.hibernate.use_sql_comments=false
+spring.jpa.properties.hibernate.format_sql=false
diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql
new file mode 100644
index 0000000..e69de29
diff --git a/src/test/java/eu/dnetlib/apps/oai/OaiServerApplicationTests.java b/src/test/java/eu/dnetlib/apps/oai/OaiServerApplicationTests.java
new file mode 100644
index 0000000..59ec099
--- /dev/null
+++ b/src/test/java/eu/dnetlib/apps/oai/OaiServerApplicationTests.java
@@ -0,0 +1,12 @@
+package eu.dnetlib.apps.oai;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+public class OaiServerApplicationTests {
+
+ @Test
+ void contextLoads() {}
+
+}