Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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 org.dspace.app.itemimport;

/**
* Thrown when a SAF package claims an embargo ({@code dc.rights.access=embargoedAccess} or
* {@code dc.date.embargoend}) that the import cannot turn into a resource policy. It is checked because the
* package has to be refused: an item archived without its embargo policy gets the collection default READ
* policy from {@code installItem} and its files become public.
*/
public class EmbargoMetadataException extends Exception {

private static final long serialVersionUID = 1L;

/**
* @param message what the package asks for and why it cannot be done
*/
public EmbargoMetadataException(String message) {
super(message);
}

/**
* @param message what the package asks for and why it cannot be done
* @param cause the underlying failure
*/
public EmbargoMetadataException(String message, Throwable cause) {
super(message, cause);
}
}

Large diffs are not rendered by default.

370 changes: 291 additions & 79 deletions dspace-api/src/main/java/org/dspace/app/itemupdate/ItemUpdate.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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 org.dspace.app.util;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.dspace.core.Constants;
import org.dspace.license.service.CreativeCommonsService;

/**
* Constants of the embargo resource policies written by the SAF batch tools, declared once so that
* {@code dspace import} and {@code dspace itemupdate} cannot drift apart.
*/
public final class SafEmbargoConstants {

/**
* Value written to {@code resourcepolicy.rpname} on embargo policies. It is the name of the
* {@code embargoed} access condition, as written by the submission UI and {@code bulk-access-control}, and
* it fits the 30 character {@code rpname} column, which a longer name would overflow.
*/
public static final String EMBARGO_POLICY_NAME = "embargo";

/** Bundle holding the text extracted from a file; {@link Constants} has no name for it. */
public static final String TEXT_BUNDLE_NAME = "TEXT";

/** Bundle holding the thumbnail rendered from a file; {@link Constants} has no name for it. */
public static final String THUMBNAIL_BUNDLE_NAME = "THUMBNAIL";

/**
* Bundles an embargo never covers, the same three {@code DefaultEmbargoSetter} leaves world readable.
* Everything else is covered: the file, the thumbnail and the extracted full text disclose the embargoed
* work, and so does a file an operator routed into a bundle of their own with the SAF
* {@code bundle:<name>} marker. {@code filter.*.publicPermission} is ignored on purpose.
*/
public static final List<String> NON_EMBARGOED_BUNDLE_NAMES = Collections.unmodifiableList(Arrays.asList(
Constants.LICENSE_BUNDLE_NAME, CreativeCommonsService.CC_BUNDLE_NAME,
Constants.METADATA_BUNDLE_NAME));

/**
* Whether an embargo covers the bundle.
*
* @param bundleName name of the bundle
* @return false for the licence and metadata bundles, true for everything that holds the work itself
*/
public static boolean isEmbargoed(String bundleName) {
return !NON_EMBARGOED_BUNDLE_NAMES.contains(bundleName);
}

private SafEmbargoConstants() {
}
}
126 changes: 126 additions & 0 deletions dspace-api/src/main/java/org/dspace/app/util/SafEmbargoDateParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* 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 org.dspace.app.util;

import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;

import org.apache.commons.lang3.StringUtils;

/**
* Turns the {@code dc.date.embargoend} value of a SAF package into the UTC calendar day on which the embargo
* ends, for {@code dspace import} and {@code dspace itemupdate} alike.
*
* <p>Parsing is strict, unlike the {@link org.dspace.content.DCDate} both tools used before, which rolls
* {@code 2026-02-30} over into 2 March and so turns a typo into a real embargo date. The shapes
* {@code DCDate} accepted are still read, and read as the same day, so existing SAF packages keep working.</p>
*/
public final class SafEmbargoDateParser {

/**
* {@code yyyy-MM-dd'T'HH[:mm[:ss[.fff]]]['Z']}, the ISO shapes {@code DCDate} accepted. Always read as
* UTC, which is what {@code DCDate} assumed for the shapes without a trailing {@code Z}.
*/
private static final DateTimeFormatter LEGACY_TIMESTAMP = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.appendLiteral('T')
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.optionalStart().appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.optionalStart().appendLiteral(':').appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
.optionalEnd().optionalEnd().optionalEnd()
.optionalStart().appendLiteral('Z').optionalEnd()
.toFormatter().withResolverStyle(ResolverStyle.STRICT);

/**
* {@code yyyy-M-d} with unpadded month and day, which {@code SimpleDateFormat} accepted and older SAF
* packages therefore contain. Strict all the same: {@code 2027-2-30} is rejected.
*/
private static final DateTimeFormatter UNPADDED_DATE = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH)
.toFormatter().withResolverStyle(ResolverStyle.STRICT);

/**
* {@code yyyy-M} with an unpadded month, which {@code SimpleDateFormat} accepted for the same reason.
*/
private static final DateTimeFormatter UNPADDED_YEAR_MONTH = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR)
.toFormatter().withResolverStyle(ResolverStyle.STRICT);

/** Listed in operator messages, so that the two tools describe the same set of values. */
public static final String ACCEPTED_FORMATS =
"yyyy-MM-dd, yyyy-MM (first of the month), yyyy (1 January) or yyyy-MM-dd'T'HH[:mm[:ss]][Z]";

private SafEmbargoDateParser() {
}

/**
* The UTC calendar day on which the embargo ends, i.e. the last day the files stay closed.
*
* @param value raw {@code dc.date.embargoend}, surrounding whitespace is ignored
* @return the embargo end day
* @throws DateTimeParseException if the value is none of the accepted shapes; a caller that cannot read
* the date has to refuse the package instead of assuming no embargo
*/
public static LocalDate parseEmbargoEndDay(String value) {
String trimmed = StringUtils.trimToEmpty(value);

try {
// yyyy-MM-dd, the shape DSpace itself writes
return LocalDate.parse(trimmed);
} catch (DateTimeParseException notAnIsoDay) {
// an older shape or garbage, decided below
}

try {
return LocalDate.parse(trimmed, LEGACY_TIMESTAMP);
} catch (DateTimeParseException notAnIsoTimestamp) {
// ditto
}

try {
// DCDate.toDate() reported a bare month as its first day, so it keeps meaning that day
return YearMonth.parse(trimmed).atDay(1);
} catch (DateTimeParseException notAYearMonth) {
// ditto
}

try {
// and a bare year as 1 January, for the same reason
return Year.parse(trimmed).atDay(1);
} catch (DateTimeParseException notAYear) {
// ditto
}

try {
return LocalDate.parse(trimmed, UNPADDED_DATE);
} catch (DateTimeParseException notAnUnpaddedDay) {
// ditto
}

try {
return YearMonth.parse(trimmed, UNPADDED_YEAR_MONTH).atDay(1);
} catch (DateTimeParseException notAnUnpaddedYearMonth) {
throw new DateTimeParseException("Unparseable embargo end date '" + value + "', expected "
+ ACCEPTED_FORMATS, trimmed, notAnUnpaddedYearMonth.getErrorIndex());
}
}
}
Loading
Loading