io.gdcc.xoai.dataprovider.repository.RepositoryConfiguration Maven / Gradle / Ivy
/*
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package io.gdcc.xoai.dataprovider.repository;
import static java.util.Arrays.asList;
import io.gdcc.xoai.dataprovider.exceptions.InternalOAIException;
import io.gdcc.xoai.model.oaipmh.DeletedRecord;
import io.gdcc.xoai.model.oaipmh.Granularity;
import io.gdcc.xoai.services.api.ResumptionTokenFormat;
import io.gdcc.xoai.services.impl.SimpleResumptionTokenFormat;
import io.gdcc.xoai.xml.WriterContext;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* RepositoryConfiguration is a class containing all settings relevant for OAI-PMH operation. A
* configuration can only be built using the {@link RepositoryConfigurationBuilder} and is
* non-modifiable. In case you need to change something, you need to build a new one.
*/
public class RepositoryConfiguration implements WriterContext {
private final List adminEmails = new ArrayList<>();
private final List descriptions = new ArrayList<>();
private final List compressions = new ArrayList<>();
private final Granularity granularity;
private final ResumptionTokenFormat resumptionTokenFormat;
private final String repositoryName;
private final String baseUrl;
private final Instant earliestDate;
private final Integer maxListIdentifiers;
private final Integer maxListSets;
private final Integer maxListRecords;
private final DeletedRecord deleteMethod;
private final boolean enableMetadataAttributes;
private final boolean requireFromAfterEarliest;
RepositoryConfiguration(
List adminEmails,
List descriptions,
List compressions,
Granularity granularity,
ResumptionTokenFormat resumptionTokenFormat,
String repositoryName,
String baseUrl,
Instant earliestDate,
Integer maxListIdentifiers,
Integer maxListSets,
Integer maxListRecords,
DeletedRecord deleteMethod,
boolean enableMetadataAttributes,
boolean requireFromAfterEarliest) {
this.adminEmails.addAll(List.copyOf(adminEmails));
this.descriptions.addAll(List.copyOf(descriptions));
this.compressions.addAll(List.copyOf(compressions));
this.granularity = granularity;
this.resumptionTokenFormat = resumptionTokenFormat;
this.repositoryName = repositoryName;
this.baseUrl = baseUrl;
this.earliestDate = earliestDate;
this.maxListIdentifiers = maxListIdentifiers;
this.maxListSets = maxListSets;
this.maxListRecords = maxListRecords;
this.deleteMethod = deleteMethod;
this.enableMetadataAttributes = enableMetadataAttributes;
this.requireFromAfterEarliest = requireFromAfterEarliest;
}
/**
* Transform an existing configuration back to a builder as a template for reconfiguration.
*
* @return A new configuration builder
*/
public RepositoryConfigurationBuilder asTemplate() {
var builder =
new RepositoryConfigurationBuilder()
.setAdminEmails(this.adminEmails)
.withGranularity(this.granularity)
.withResumptionTokenFormat(this.resumptionTokenFormat)
.withRepositoryName(this.repositoryName)
.withBaseUrl(this.baseUrl)
.withEarliestDate(this.earliestDate)
.withMaxListIdentifiers(this.maxListIdentifiers)
.withMaxListSets(this.maxListSets)
.withMaxListRecords(this.maxListRecords)
.withDeleteMethod(this.deleteMethod)
.withEnableMetadataAttributes(this.enableMetadataAttributes)
.withRequireFromAfterEarliest(this.requireFromAfterEarliest);
// Quick and hacky addition as no methods for bulk adding available
builder.descriptions.clear();
builder.descriptions.addAll(this.descriptions);
builder.compressions.clear();
builder.compressions.addAll(this.compressions);
return builder;
}
public void inject(Repository repository) {
repository.setConfiguration(this);
}
public String getRepositoryName() {
if (repositoryName == null)
throw new InternalOAIException("Repository name has not been configured");
return repositoryName;
}
public List getAdminEmails() {
return Collections.unmodifiableList(adminEmails);
}
public String getBaseUrl() {
if (baseUrl == null)
throw new InternalOAIException("Repository base URL has not been configured");
return baseUrl;
}
public Instant getEarliestDate() {
if (earliestDate == null)
throw new InternalOAIException("Earliest date has not been configured");
return earliestDate;
}
public int getMaxListIdentifiers() {
if (maxListIdentifiers == null)
throw new InternalOAIException("Maximum number of identifiers has not been configured");
return maxListIdentifiers;
}
public int getMaxListSets() {
if (maxListSets == null)
throw new InternalOAIException("Maximum number of sets has not been configured");
return maxListSets;
}
public int getMaxListRecords() {
if (maxListRecords == null)
throw new InternalOAIException("Maximum number of records has not been configured");
return maxListRecords;
}
@Override
public Granularity getGranularity() {
if (granularity == null)
throw new InternalOAIException("Granularity has not been configured");
return granularity;
}
/**
* Skew an instant to end of day (granularity Day or Lenient) or by a second (granularity
* Second). This is necessary for two reasons: 1. Day granularity must be inclusive, so we can't
* leave an "until" at start of day. 2. Lenient granularity must work like day in this case, as
* we cannot be sure which is meant. 3. Second granularity needs skipping a second to avoid not
* returning {@link io.gdcc.xoai.dataprovider.model.Item} from the repository that use an SQL
* timestamp with nanosecond granularity (so ...:00.5829 would not be found when asking for all
* until ...:00.000)
*
* @param timestamp The timestamp to skew a little
* @return The skewed timestamp
*/
public Instant skewUntil(Instant timestamp) {
Objects.requireNonNull(timestamp, "Skewing an 'until' date must not be used with null");
switch (getGranularity()) {
case Day:
case Lenient:
return LocalDate.ofInstant(timestamp, ZoneId.of("UTC"))
.atTime(LocalTime.MAX)
.toInstant(ZoneOffset.UTC);
case Second:
return timestamp.plusSeconds(1);
default:
return timestamp;
}
}
public DeletedRecord getDeleteMethod() {
if (deleteMethod == null)
throw new InternalOAIException("Delete method has not been configured");
return deleteMethod;
}
public List getDescription() {
return Collections.unmodifiableList(descriptions);
}
public List getCompressions() {
return Collections.unmodifiableList(compressions);
}
public boolean hasCompressions() {
return !compressions.isEmpty();
}
@Override
public ResumptionTokenFormat getResumptionTokenFormat() {
if (resumptionTokenFormat == null)
throw new InternalOAIException("Resumption token format has not been configured");
return this.resumptionTokenFormat;
}
@Override
public boolean isMetadataAttributesEnabled() {
return this.enableMetadataAttributes;
}
public boolean requiresFromAfterEarliest() {
return requireFromAfterEarliest;
}
public static final class RepositoryConfigurationBuilder {
/* All field below package private to access in tests */
final List adminEmails = new ArrayList<>();
final List descriptions = new ArrayList<>();
final List compressions = new ArrayList<>();
Granularity granularity = Granularity.Second;
ResumptionTokenFormat resumptionTokenFormat =
new SimpleResumptionTokenFormat().withGranularity(Granularity.Second);
String repositoryName;
String baseUrl;
Instant earliestDate;
DeletedRecord deleteMethod;
Integer maxListIdentifiers = 100;
Integer maxListSets = 100;
Integer maxListRecords = 100;
Boolean enableMetadataAttributes = false;
Boolean requireFromAfterEarliest = false;
public RepositoryConfigurationBuilder withGranularity(Granularity granularity) {
this.requireNotNull(granularity, "Granularity must not be null");
this.granularity = granularity;
return this;
}
public RepositoryConfigurationBuilder withRepositoryName(String repositoryName) {
this.requireNotNullNotEmpty(
repositoryName, "Repository name must not be null or empty");
this.repositoryName = repositoryName;
return this;
}
public RepositoryConfigurationBuilder setAdminEmails(List emails) {
this.requireNotNull(emails, "Admin emails list must not be null");
if (emails.isEmpty()) {
throw new IllegalArgumentException("Admin emails list must not be empty");
}
return this.setAdminEmails(emails.toArray(String[]::new));
}
public RepositoryConfigurationBuilder setAdminEmails(String... emails) {
// Backup first, then clear and add. In case the verification fails, restore backup
var backup = List.copyOf(this.adminEmails);
this.adminEmails.clear();
try {
return this.withAdminEmails(emails);
} catch (IllegalArgumentException e) {
this.adminEmails.addAll(backup);
throw e;
}
}
public RepositoryConfigurationBuilder withAdminEmails(String... emails) {
this.requireNotNull(emails, "Admin email list must not be null");
if (emails.length == 0) {
throw new IllegalArgumentException("Admin emails list must not be empty");
}
for (String s : emails) {
this.requireNotNullNotEmpty(
s,
"Admin email must not be null or empty in list ('"
+ String.join("', '", emails)
+ "')");
// TODO: one might add mail verification here
}
this.adminEmails.addAll(asList(emails));
return this;
}
public RepositoryConfigurationBuilder withAdminEmail(String email) {
this.requireNotNullNotEmpty(email, "Admin email must not be null or empty");
this.adminEmails.add(email);
return this;
}
public RepositoryConfigurationBuilder withDeleteMethod(DeletedRecord deleteMethod) {
this.requireNotNull(deleteMethod, "Deletion Method must not be null");
this.deleteMethod = deleteMethod;
return this;
}
public RepositoryConfigurationBuilder withDescription(String description) {
this.requireNotNullNotEmpty(description, "Description must not be null or empty");
descriptions.add(description);
return this;
}
public RepositoryConfigurationBuilder withBaseUrl(String baseUrl) {
this.requireNotNullNotEmpty(baseUrl, "Base URL must not be null or empty");
this.baseUrl = baseUrl;
return this;
}
public RepositoryConfigurationBuilder withEarliestDate(Instant earliestDate) {
this.requireNotNull(earliestDate, "Earliest date must not be null");
if (earliestDate.isAfter(Instant.now())) {
throw new IllegalArgumentException(
"Earliest date cannot lie in the future (given: "
+ earliestDate.truncatedTo(ChronoUnit.SECONDS).toString()
+ ")");
}
this.earliestDate = earliestDate;
return this;
}
public RepositoryConfigurationBuilder withCompression(String compression) {
this.requireNotNullNotEmpty(compression, "Compression must not be null or empty");
compressions.add(compression);
return this;
}
public RepositoryConfigurationBuilder withMaxListRecords(int maxListRecords) {
if (maxListRecords < 1) {
throw new IllegalArgumentException(
"Maximum ListRecords response size must be greater 0");
}
this.maxListRecords = maxListRecords;
return this;
}
public RepositoryConfigurationBuilder withMaxListIdentifiers(int maxListIdentifiers) {
if (maxListIdentifiers < 1) {
throw new IllegalArgumentException(
"Maximum ListIdentifiers response size must be greater 0");
}
this.maxListIdentifiers = maxListIdentifiers;
return this;
}
public RepositoryConfigurationBuilder withMaxListSets(int maxListSets) {
if (maxListSets < 1) {
throw new IllegalArgumentException(
"Maximum ListSets response size must be greater 0");
}
this.maxListSets = maxListSets;
return this;
}
public RepositoryConfigurationBuilder withResumptionTokenFormat(
ResumptionTokenFormat format) {
this.requireNotNull(format, "Resumption Token Format must not be null");
this.resumptionTokenFormat = format;
return this;
}
/**
* This is here for Dataverse 4/5 backward compatibility.
*
* They added an attribute to the >record<<metadata>
element,
* containing the API URL of a record in their special metadata format "dataverse_json".
*
* @deprecated Remove when Dataverse 6 is old enough that no ones uses this workaround
* anymore.
*/
@Deprecated(since = "5.0")
public RepositoryConfigurationBuilder withEnableMetadataAttributes(boolean enable) {
this.enableMetadataAttributes = enable;
return this;
}
/**
* Configure if any "from" parameter must be required to be a point in time after the
* earliest date of the repo (the oldest item). This is not strictly required by the OAI-PMH
* spec, but was the default behaviour of XOAI 3 and 4. Setting to true restores the
* behaviour.
*/
public RepositoryConfigurationBuilder withRequireFromAfterEarliest(boolean require) {
this.requireFromAfterEarliest = require;
return this;
}
public RepositoryConfiguration build() {
// Basic validation of configuration that is still missing
// 1. At least 1 admin mail present?
if (adminEmails.isEmpty()) {
throw new IllegalArgumentException("Missing admin email address/es");
}
// 2. Parameters without a default should have been set
this.requireNotNullNotEmpty(baseUrl, "Missing base URL");
this.requireNotNullNotEmpty(repositoryName, "Missing repository name");
this.requireNotNull(
earliestDate,
"Missing 'earliest date', which is the date of the first inserted item");
this.requireNotNull(deleteMethod, "Missing delete method");
return new RepositoryConfiguration(
adminEmails,
descriptions,
compressions,
granularity,
resumptionTokenFormat,
repositoryName,
baseUrl,
earliestDate,
maxListIdentifiers,
maxListSets,
maxListRecords,
deleteMethod,
enableMetadataAttributes,
requireFromAfterEarliest);
}
/**
* Check for null and throw an IllegalArgumentException with a message when found.
*
* @param o Subject to test
* @param message The message if subject is null
* @throws IllegalArgumentException If subject is null
*/
public void requireNotNull(Object o, String message) {
if (o == null) {
throw new IllegalArgumentException(message);
}
}
/**
* Check for null or an empty String and throw an IllegalArgumentException with a message
* when found.
*
* @param s Subject to test
* @param message The message if subject is null
* @throws IllegalArgumentException If subject is null or empty String
*/
public void requireNotNullNotEmpty(String s, String message) {
if (s == null || s.isEmpty()) {
throw new IllegalArgumentException(message);
}
}
}
}