policies = authorizeService
+ .getPoliciesActionFilter(context, bitstream, Constants.READ);
+
+ // Looks at all read policies.
+ for (ResourcePolicy policy : policies) {
+ boolean isValid = resourcePolicyService.isDateValid(policy);
+ Group group = policy.getGroup();
+
+ if (group != null && StringUtils.equals(group.getName(), Group.ANONYMOUS)) {
+ // Only calculate the status for the anonymous group.
+ if (!isValid) {
+ // If the policy is not valid there is an active embargo
+ Date startDate = policy.getStartDate();
+
+ if (startDate != null && !startDate.before(LocalDate.now().toDate())) {
+ // There is an active embargo: aim to take the shortest embargo (account for rare cases where
+ // more than one resource policy exists)
+ if (embargoDate == null) {
+ embargoDate = startDate;
+ } else {
+ embargoDate = startDate.before(embargoDate) ? startDate : embargoDate;
+ }
+ }
+ }
+ }
+ }
+
+ return embargoDate;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactory.java b/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactory.java
new file mode 100644
index 000000000000..77d8f6b44876
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactory.java
@@ -0,0 +1,25 @@
+/**
+ * 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.access.status.factory;
+
+import org.dspace.access.status.service.AccessStatusService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+
+/**
+ * Abstract factory to get services for the access status package,
+ * use AccessStatusServiceFactory.getInstance() to retrieve an implementation.
+ */
+public abstract class AccessStatusServiceFactory {
+
+ public abstract AccessStatusService getAccessStatusService();
+
+ public static AccessStatusServiceFactory getInstance() {
+ return DSpaceServicesFactory.getInstance().getServiceManager()
+ .getServiceByName("accessStatusServiceFactory", AccessStatusServiceFactory.class);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactoryImpl.java
new file mode 100644
index 000000000000..fe3848cb2b21
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/access/status/factory/AccessStatusServiceFactoryImpl.java
@@ -0,0 +1,26 @@
+/**
+ * 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.access.status.factory;
+
+import org.dspace.access.status.service.AccessStatusService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Factory implementation to get services for the access status package,
+ * use AccessStatusServiceFactory.getInstance() to retrieve an implementation.
+ */
+public class AccessStatusServiceFactoryImpl extends AccessStatusServiceFactory {
+
+ @Autowired(required = true)
+ private AccessStatusService accessStatusService;
+
+ @Override
+ public AccessStatusService getAccessStatusService() {
+ return accessStatusService;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/access/status/package-info.java b/dspace-api/src/main/java/org/dspace/access/status/package-info.java
new file mode 100644
index 000000000000..2c0ed22cd4a9
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/access/status/package-info.java
@@ -0,0 +1,30 @@
+/**
+ * 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/
+ */
+/**
+ *
+ * Access status allows the users to view the bitstreams availability before
+ * browsing into the item itself.
+ *
+ *
+ * The access status is calculated through a pluggable class:
+ * {@link org.dspace.access.status.AccessStatusHelper}.
+ * The {@link org.dspace.access.status.AccessStatusServiceImpl}
+ * must be configured to specify this class, as well as a forever embargo date
+ * threshold year, month and day.
+ *
+ *
+ * See {@link org.dspace.access.status.DefaultAccessStatusHelper} for a simple calculation
+ * based on the primary or the first bitstream of the original bundle. You can
+ * supply your own class to implement more complex access statuses.
+ *
+ *
+ * For now, the access status is calculated when the item is shown in a list.
+ *
+ */
+
+package org.dspace.access.status;
diff --git a/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java
new file mode 100644
index 000000000000..2ed47bde4cd2
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java
@@ -0,0 +1,57 @@
+/**
+ * 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.access.status.service;
+
+import java.sql.SQLException;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * Public interface to the access status subsystem.
+ *
+ * Configuration properties: (with examples)
+ * {@code
+ * # values for the forever embargo date threshold
+ * # This threshold date is used in the default access status helper to dermine if an item is
+ * # restricted or embargoed based on the start date of the primary (or first) file policies.
+ * # In this case, if the policy start date is inferior to the threshold date, the status will
+ * # be embargo, else it will be restricted.
+ * # You might want to change this threshold based on your needs. For example: some databases
+ * # doesn't accept a date superior to 31 december 9999.
+ * access.status.embargo.forever.year = 10000
+ * access.status.embargo.forever.month = 1
+ * access.status.embargo.forever.day = 1
+ * # implementation of access status helper plugin - replace with local implementation if applicable
+ * # This default access status helper provides an item status based on the policies of the primary
+ * # bitstream (or first bitstream in the original bundles if no primary file is specified).
+ * plugin.single.org.dspace.access.status.AccessStatusHelper = org.dspace.access.status.DefaultAccessStatusHelper
+ * }
+ */
+public interface AccessStatusService {
+
+ /**
+ * Calculate the access status for an Item while considering the forever embargo date threshold.
+ *
+ * @param context the DSpace context
+ * @param item the item
+ * @return an access status value
+ * @throws SQLException An exception that provides information on a database access error or other errors.
+ */
+ public String getAccessStatus(Context context, Item item) throws SQLException;
+
+ /**
+ * Retrieve embargo information for the item
+ *
+ * @param context the DSpace context
+ * @param item the item to check for embargo information
+ * @return an embargo date
+ * @throws SQLException An exception that provides information on a database access error or other errors.
+ */
+ public String getEmbargoFromItem(Context context, Item item) throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java
index 80d69f3b661b..0006f5c01afd 100644
--- a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java
+++ b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java
@@ -14,8 +14,13 @@
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.lang3.StringUtils;
+import org.dspace.content.clarin.ClarinUserRegistration;
+import org.dspace.content.factory.ClarinServiceFactory;
+import org.dspace.content.service.clarin.ClarinUserRegistrationService;
import org.dspace.core.Context;
import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
@@ -50,18 +55,22 @@ public final class CreateAdministrator {
*/
private final Context context;
+ private static final Option OPT_ORGANIZATION = new Option("o", "organization", true,
+ "organization the user belongs to");
+
protected EPersonService ePersonService;
protected GroupService groupService;
+ protected ClarinUserRegistrationService clarinUserRegistrationService;
/**
- * For invoking via the command line. If called with no command line arguments,
+ * For invoking via the command line. If called with no command line arguments,
* it will negotiate with the user for the administrator details
*
* @param argv the command line arguments given
* @throws Exception if error
*/
public static void main(String[] argv)
- throws Exception {
+ throws Exception {
CommandLineParser parser = new DefaultParser();
Options options = new Options();
@@ -69,19 +78,43 @@ public static void main(String[] argv)
options.addOption("e", "email", true, "administrator email address");
options.addOption("f", "first", true, "administrator first name");
+ options.addOption("h", "help", false, "explain create-administrator options");
options.addOption("l", "last", true, "administrator last name");
options.addOption("c", "language", true, "administrator language");
options.addOption("p", "password", true, "administrator password");
+ options.addOption(OPT_ORGANIZATION);
+
+ CommandLine line = null;
+
+ try {
+
+ line = parser.parse(options, argv);
- CommandLine line = parser.parse(options, argv);
+ } catch (Exception e) {
+
+ System.out.println(e.getMessage() + "\nTry \"dspace create-administrator -h\" to print help information.");
+ System.exit(1);
+
+ }
if (line.hasOption("e") && line.hasOption("f") && line.hasOption("l") &&
- line.hasOption("c") && line.hasOption("p")) {
+ line.hasOption("c") && line.hasOption("p") && line.hasOption("o")) {
ca.createAdministrator(line.getOptionValue("e"),
- line.getOptionValue("f"), line.getOptionValue("l"),
- line.getOptionValue("c"), line.getOptionValue("p"));
+ line.getOptionValue("f"), line.getOptionValue("l"),
+ line.getOptionValue("c"), line.getOptionValue("p"),
+ line.getOptionValue("o"));
+ } else if (line.hasOption("h")) {
+ String header = "\nA command-line tool for creating an initial administrator for setting up a" +
+ " DSpace site. Unless all the required parameters are passed it will" +
+ " prompt for an e-mail address, last name, first name and password from" +
+ " standard input.. An administrator group is then created and the data passed" +
+ " in used to create an e-person in that group.\n\n";
+ String footer = "\n";
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("dspace create-administrator", header, options, footer, true);
+ return;
} else {
- ca.negotiateAdministratorDetails();
+ ca.negotiateAdministratorDetails(line);
}
}
@@ -91,10 +124,22 @@ public static void main(String[] argv)
* @throws Exception if error
*/
protected CreateAdministrator()
- throws Exception {
+ throws Exception {
context = new Context();
+ try {
+ context.getDBConfig();
+ } catch (NullPointerException npr) {
+ // if database is null, there is no point in continuing. Prior to this exception and catch,
+ // NullPointerException was thrown, that wasn't very helpful.
+ throw new IllegalStateException("Problem connecting to database. This" +
+ " indicates issue with either network or version (or possibly some other). " +
+ "If you are running this in docker-compose, please make sure dspace-cli was " +
+ "built from the same sources as running dspace container AND that they are in " +
+ "the same project/network.");
+ }
groupService = EPersonServiceFactory.getInstance().getGroupService();
ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
+ clarinUserRegistrationService = ClarinServiceFactory.getInstance().getClarinUserRegistration();
}
/**
@@ -103,20 +148,21 @@ protected CreateAdministrator()
*
* @throws Exception if error
*/
- protected void negotiateAdministratorDetails()
- throws Exception {
+ protected void negotiateAdministratorDetails(CommandLine line)
+ throws Exception {
Console console = System.console();
System.out.println("Creating an initial administrator account");
- boolean dataOK = false;
-
- String email = null;
- String firstName = null;
- String lastName = null;
- char[] password1 = null;
- char[] password2 = null;
+ String email = line.getOptionValue('e');
+ String firstName = line.getOptionValue('f');
+ String lastName = line.getOptionValue('l');
String language = I18nUtil.getDefaultLocale().getLanguage();
+ String org = line.getOptionValue('o');
+ ConfigurationService cfg = DSpaceServicesFactory.getInstance().getConfigurationService();
+ boolean flag = line.hasOption('p');
+ char[] password = null;
+ boolean dataOK = line.hasOption('f') && line.hasOption('e') && line.hasOption('l');
while (!dataOK) {
System.out.print("E-mail address: ");
@@ -147,8 +193,6 @@ protected void negotiateAdministratorDetails()
if (lastName != null) {
lastName = lastName.trim();
}
-
- ConfigurationService cfg = DSpaceServicesFactory.getInstance().getConfigurationService();
if (cfg.hasProperty("webui.supported.locales")) {
System.out.println("Select one of the following languages: "
+ cfg.getProperty("webui.supported.locales"));
@@ -163,46 +207,58 @@ protected void negotiateAdministratorDetails()
}
}
- System.out.println("Password will not display on screen.");
- System.out.print("Password: ");
+ System.out.print("Is the above data correct? (y or n): ");
System.out.flush();
- password1 = console.readPassword();
+ String s = console.readLine();
- System.out.print("Again to confirm: ");
- System.out.flush();
+ if (s != null) {
+ s = s.trim();
+ if (s.toLowerCase().startsWith("y")) {
+ dataOK = true;
+ }
+ }
- password2 = console.readPassword();
+ }
+ if (!flag) {
+ password = getPassword(console);
+ if (password == null) {
+ return;
+ }
+ } else {
+ password = line.getOptionValue("p").toCharArray();
+ }
+ // if we make it to here, we are ready to create an administrator
+ createAdministrator(email, firstName, lastName, language, String.valueOf(password), org);
+ }
- //TODO real password validation
- if (password1.length > 1 && Arrays.equals(password1, password2)) {
- // password OK
- System.out.print("Is the above data correct? (y or n): ");
- System.out.flush();
+ private char[] getPassword(Console console) {
+ char[] password1 = null;
+ char[] password2 = null;
+ System.out.println("Password will not display on screen.");
+ System.out.print("Password: ");
+ System.out.flush();
- String s = console.readLine();
+ password1 = console.readPassword();
- if (s != null) {
- s = s.trim();
- if (s.toLowerCase().startsWith("y")) {
- dataOK = true;
- }
- }
- } else {
- System.out.println("Passwords don't match");
- }
- }
+ System.out.print("Again to confirm: ");
+ System.out.flush();
- // if we make it to here, we are ready to create an administrator
- createAdministrator(email, firstName, lastName, language, String.valueOf(password1));
+ password2 = console.readPassword();
- //Cleaning arrays that held password
- Arrays.fill(password1, ' ');
- Arrays.fill(password2, ' ');
+ // TODO real password validation
+ if (password1.length > 1 && Arrays.equals(password1, password2)) {
+ // password OK
+ Arrays.fill(password2, ' ');
+ return password1;
+ } else {
+ System.out.println("Passwords don't match");
+ return null;
+ }
}
/**
- * Create the administrator with the given details. If the user
+ * Create the administrator with the given details. If the user
* already exists then they are simply upped to administrator status
*
* @param email the email for the user
@@ -213,7 +269,7 @@ protected void negotiateAdministratorDetails()
* @throws Exception if error
*/
protected void createAdministrator(String email, String first, String last,
- String language, String pw)
+ String language, String pw, String organization)
throws Exception {
// Of course we aren't an administrator yet so we need to
// circumvent authorisation
@@ -248,6 +304,13 @@ protected void createAdministrator(String email, String first, String last,
groupService.addMember(context, admins, eperson);
groupService.update(context, admins);
+ ClarinUserRegistration clarinUserRegistration = new ClarinUserRegistration();
+ clarinUserRegistration.setOrganization(organization);
+ clarinUserRegistration.setConfirmation(true);
+ clarinUserRegistration.setEmail(eperson.getEmail());
+ clarinUserRegistration.setPersonID(eperson.getID());
+ clarinUserRegistrationService.create(context, clarinUserRegistration);
+
context.complete();
System.out.println("Administrator account created");
diff --git a/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java b/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java
new file mode 100644
index 000000000000..fb592627adef
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/FileDownloader.java
@@ -0,0 +1,229 @@
+/**
+ * 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.administer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import org.apache.commons.cli.ParseException;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.Bitstream;
+import org.dspace.content.BitstreamFormat;
+import org.dspace.content.Bundle;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.BitstreamFormatService;
+import org.dspace.content.service.BitstreamService;
+import org.dspace.content.service.ItemService;
+import org.dspace.content.service.WorkspaceItemService;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.dspace.eperson.factory.EPersonServiceFactory;
+import org.dspace.eperson.service.EPersonService;
+import org.dspace.identifier.IdentifierNotFoundException;
+import org.dspace.identifier.IdentifierNotResolvableException;
+import org.dspace.identifier.factory.IdentifierServiceFactory;
+import org.dspace.identifier.service.IdentifierService;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+import org.dspace.utils.DSpace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class FileDownloader extends DSpaceRunnable {
+
+ private static final Logger log = LoggerFactory.getLogger(FileDownloader.class);
+ private boolean help = false;
+ private UUID itemUUID;
+ private int workspaceID;
+ private String pid;
+ private URI uri;
+ private String epersonMail;
+ private String bitstreamName;
+ private EPersonService epersonService;
+ private ItemService itemService;
+ private WorkspaceItemService workspaceItemService;
+ private IdentifierService identifierService;
+ private BitstreamService bitstreamService;
+ private BitstreamFormatService bitstreamFormatService;
+ private final HttpClient httpClient = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.NORMAL)
+ .build();
+
+ /**
+ * This method will return the Configuration that the implementing DSpaceRunnable uses
+ *
+ * @return The {@link ScriptConfiguration} that this implementing DspaceRunnable uses
+ */
+ @Override
+ public FileDownloaderConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager().getServiceByName("file-downloader",
+ FileDownloaderConfiguration.class);
+ }
+
+ /**
+ * This method has to be included in every script and handles the setup of the script by parsing the CommandLine
+ * and setting the variables
+ *
+ * @throws ParseException If something goes wrong
+ */
+ @Override
+ public void setup() throws ParseException {
+ log.debug("Setting up {}", FileDownloader.class.getName());
+ if (commandLine.hasOption("h")) {
+ help = true;
+ return;
+ }
+
+ if (!commandLine.hasOption("u")) {
+ throw new ParseException("No URL option has been provided");
+ }
+
+ if (!commandLine.hasOption("i") && !commandLine.hasOption("w") && !commandLine.hasOption("p")) {
+ throw new ParseException("No item id option has been provided");
+ }
+
+ if (getEpersonIdentifier() == null && !commandLine.hasOption("e")) {
+ throw new ParseException("No eperson option has been provided");
+ }
+
+
+ this.epersonService = EPersonServiceFactory.getInstance().getEPersonService();
+ this.itemService = ContentServiceFactory.getInstance().getItemService();
+ this.workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
+ this.bitstreamService = ContentServiceFactory.getInstance().getBitstreamService();
+ this.bitstreamFormatService = ContentServiceFactory.getInstance().getBitstreamFormatService();
+ this.identifierService = IdentifierServiceFactory.getInstance().getIdentifierService();
+
+ try {
+ uri = new URI(commandLine.getOptionValue("u"));
+ } catch (URISyntaxException e) {
+ throw new ParseException("The provided URL is not a valid URL");
+ }
+
+ if (commandLine.hasOption("i")) {
+ itemUUID = UUID.fromString(commandLine.getOptionValue("i"));
+ } else if (commandLine.hasOption("w")) {
+ workspaceID = Integer.parseInt(commandLine.getOptionValue("w"));
+ } else if (commandLine.hasOption("p")) {
+ pid = commandLine.getOptionValue("p");
+ }
+
+ epersonMail = commandLine.getOptionValue("e");
+
+ if (commandLine.hasOption("n")) {
+ bitstreamName = commandLine.getOptionValue("n");
+ }
+ }
+
+ /**
+ * This method has to be included in every script and this will be the main execution block for the script that'll
+ * contain all the logic needed
+ *
+ * @throws Exception If something goes wrong
+ */
+ @Override
+ public void internalRun() throws Exception {
+ log.debug("Running {}", FileDownloader.class.getName());
+ if (help) {
+ printHelp();
+ return;
+ }
+
+ Context context = new Context();
+ context.setCurrentUser(getEperson(context));
+
+ //find the item by the given id
+ Item item = findItem(context);
+ if (item == null) {
+ throw new IllegalArgumentException("No item found for the given ID");
+ }
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(uri)
+ .build();
+
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
+
+ if (response.statusCode() >= 400) {
+ throw new IllegalArgumentException("The provided URL returned a status code of " + response.statusCode());
+ }
+
+ //use the provided value, the content-disposition header, the last part of the uri
+ if (bitstreamName == null) {
+ bitstreamName = response.headers().firstValue("Content-Disposition")
+ .filter(value -> value.contains("filename=")).flatMap(value -> Stream.of(value.split(";"))
+ .filter(v -> v.contains("filename="))
+ .findFirst()
+ .map(fvalue -> fvalue.replaceFirst("filename=", "").replaceAll("\"", "")))
+ .orElse(uri.getPath().substring(uri.getPath().lastIndexOf('/') + 1));
+ }
+
+ try (InputStream is = response.body()) {
+ saveFileToItem(context, item, is, bitstreamName);
+ }
+
+ context.commit();
+ }
+
+ private Item findItem(Context context) throws SQLException {
+ if (itemUUID != null) {
+ return itemService.find(context, itemUUID);
+ } else if (workspaceID != 0) {
+ return workspaceItemService.find(context, workspaceID).getItem();
+ } else {
+ try {
+ DSpaceObject dso = identifierService.resolve(context, pid);
+ if (dso instanceof Item) {
+ return (Item) dso;
+ } else {
+ throw new IllegalArgumentException("The provided identifier does not resolve to an item");
+ }
+ } catch (IdentifierNotFoundException | IdentifierNotResolvableException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+
+ private void saveFileToItem(Context context, Item item, InputStream is, String name)
+ throws SQLException, AuthorizeException, IOException {
+ log.debug("Saving file to item {}", item.getID());
+ List originals = item.getBundles("ORIGINAL");
+ Bitstream b;
+ if (originals.isEmpty()) {
+ b = itemService.createSingleBitstream(context, is, item);
+ } else {
+ Bundle bundle = originals.get(0);
+ b = bitstreamService.create(context, bundle, is);
+ }
+ b.setName(context, name);
+ //now guess format of the bitstream
+ BitstreamFormat bf = bitstreamFormatService.guessFormat(context, b);
+ b.setFormat(context, bf);
+ }
+
+ private EPerson getEperson(Context context) throws SQLException {
+ if (getEpersonIdentifier() != null) {
+ return epersonService.find(context, getEpersonIdentifier());
+ } else {
+ return epersonService.findByEmail(context, epersonMail);
+ }
+ }
+}
+
diff --git a/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java
new file mode 100644
index 000000000000..848b2d99f7c0
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/FileDownloaderConfiguration.java
@@ -0,0 +1,73 @@
+/**
+ * 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.administer;
+
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+public class FileDownloaderConfiguration extends ScriptConfiguration {
+
+ private Class dspaceRunnableClass;
+
+ /**
+ * Generic getter for the dspaceRunnableClass
+ *
+ * @return the dspaceRunnableClass value of this ScriptConfiguration
+ */
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableClass;
+ }
+
+ /**
+ * Generic setter for the dspaceRunnableClass
+ *
+ * @param dspaceRunnableClass The dspaceRunnableClass to be set on this IndexDiscoveryScriptConfiguration
+ */
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableClass = dspaceRunnableClass;
+ }
+
+ /**
+ * The getter for the options of the Script
+ *
+ * @return the options value of this ScriptConfiguration
+ */
+ @Override
+ public Options getOptions() {
+ if (options == null) {
+
+ Options options = new Options();
+ OptionGroup ids = new OptionGroup();
+
+ options.addOption("h", "help", false, "help");
+
+ options.addOption("u", "url", true, "source url");
+ options.getOption("u").setRequired(true);
+
+ options.addOption("i", "uuid", true, "item uuid");
+ options.addOption("w", "wsid", true, "workspace id");
+ options.addOption("p", "pid", true, "item pid (e.g. handle or doi)");
+ ids.addOption(options.getOption("i"));
+ ids.addOption(options.getOption("w"));
+ ids.addOption(options.getOption("p"));
+ ids.setRequired(true);
+
+ options.addOption("e", "eperson", true, "eperson email");
+ options.getOption("e").setRequired(false);
+
+ options.addOption("n", "name", true, "name of the file/bitstream");
+ options.getOption("n").setRequired(false);
+
+ super.options = options;
+ }
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
index 42461d721071..2677cb20501f 100644
--- a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
+++ b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java
@@ -11,13 +11,16 @@
import java.sql.SQLException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
-import org.apache.xpath.XPathAPI;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
@@ -90,7 +93,7 @@ private MetadataImporter() { }
public static void main(String[] args)
throws ParseException, SQLException, IOException, TransformerException,
ParserConfigurationException, AuthorizeException, SAXException,
- NonUniqueMetadataException, RegistryImportException {
+ NonUniqueMetadataException, RegistryImportException, XPathExpressionException {
// create an options object and populate it
CommandLineParser parser = new DefaultParser();
@@ -124,8 +127,8 @@ public static void main(String[] args)
* @throws RegistryImportException if import fails
*/
public static void loadRegistry(String file, boolean forceUpdate)
- throws SQLException, IOException, TransformerException, ParserConfigurationException,
- AuthorizeException, SAXException, NonUniqueMetadataException, RegistryImportException {
+ throws SQLException, IOException, TransformerException, ParserConfigurationException, AuthorizeException,
+ SAXException, NonUniqueMetadataException, RegistryImportException, XPathExpressionException {
Context context = null;
try {
@@ -137,7 +140,9 @@ public static void loadRegistry(String file, boolean forceUpdate)
Document document = RegistryImporter.loadXML(file);
// Get the nodes corresponding to types
- NodeList schemaNodes = XPathAPI.selectNodeList(document, "/dspace-dc-types/dc-schema");
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList schemaNodes = (NodeList) xPath.compile("/dspace-dc-types/dc-schema")
+ .evaluate(document, XPathConstants.NODESET);
// Add each one as a new format to the registry
for (int i = 0; i < schemaNodes.getLength(); i++) {
@@ -146,7 +151,8 @@ public static void loadRegistry(String file, boolean forceUpdate)
}
// Get the nodes corresponding to types
- NodeList typeNodes = XPathAPI.selectNodeList(document, "/dspace-dc-types/dc-type");
+ NodeList typeNodes = (NodeList) xPath.compile("/dspace-dc-types/dc-type")
+ .evaluate(document, XPathConstants.NODESET);
// Add each one as a new format to the registry
for (int i = 0; i < typeNodes.getLength(); i++) {
@@ -178,8 +184,8 @@ public static void loadRegistry(String file, boolean forceUpdate)
* @throws RegistryImportException if import fails
*/
private static void loadSchema(Context context, Node node, boolean updateExisting)
- throws SQLException, IOException, TransformerException,
- AuthorizeException, NonUniqueMetadataException, RegistryImportException {
+ throws SQLException, AuthorizeException, NonUniqueMetadataException, RegistryImportException,
+ XPathExpressionException {
// Get the values
String name = RegistryImporter.getElementData(node, "name");
String namespace = RegistryImporter.getElementData(node, "namespace");
@@ -236,8 +242,8 @@ private static void loadSchema(Context context, Node node, boolean updateExistin
* @throws RegistryImportException if import fails
*/
private static void loadType(Context context, Node node)
- throws SQLException, IOException, TransformerException,
- AuthorizeException, NonUniqueMetadataException, RegistryImportException {
+ throws SQLException, IOException, AuthorizeException, NonUniqueMetadataException, RegistryImportException,
+ XPathExpressionException {
// Get the values
String schema = RegistryImporter.getElementData(node, "schema");
String element = RegistryImporter.getElementData(node, "element");
diff --git a/dspace-api/src/main/java/org/dspace/administer/ProcessCleaner.java b/dspace-api/src/main/java/org/dspace/administer/ProcessCleaner.java
new file mode 100644
index 000000000000..ee6b8d08b059
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleaner.java
@@ -0,0 +1,140 @@
+/**
+ * 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.administer;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.lang.time.DateUtils;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.ProcessStatus;
+import org.dspace.core.Context;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.scripts.Process;
+import org.dspace.scripts.factory.ScriptServiceFactory;
+import org.dspace.scripts.service.ProcessService;
+import org.dspace.services.ConfigurationService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+import org.dspace.utils.DSpace;
+
+/**
+ * Script to cleanup the old processes in the specified state.
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class ProcessCleaner extends DSpaceRunnable> {
+
+ private ConfigurationService configurationService;
+
+ private ProcessService processService;
+
+
+ private boolean cleanCompleted = false;
+
+ private boolean cleanFailed = false;
+
+ private boolean cleanRunning = false;
+
+ private boolean help = false;
+
+ private Integer days;
+
+
+ @Override
+ public void setup() throws ParseException {
+
+ this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
+ this.processService = ScriptServiceFactory.getInstance().getProcessService();
+
+ this.help = commandLine.hasOption('h');
+ this.cleanFailed = commandLine.hasOption('f');
+ this.cleanRunning = commandLine.hasOption('r');
+ this.cleanCompleted = commandLine.hasOption('c') || (!cleanFailed && !cleanRunning);
+
+ this.days = configurationService.getIntProperty("process-cleaner.days", 14);
+
+ if (this.days <= 0) {
+ throw new IllegalStateException("The number of days must be a positive integer.");
+ }
+
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+
+ if (help) {
+ printHelp();
+ return;
+ }
+
+ Context context = new Context();
+
+ try {
+ context.turnOffAuthorisationSystem();
+ performDeletion(context);
+ } finally {
+ context.restoreAuthSystemState();
+ context.complete();
+ }
+
+ }
+
+ /**
+ * Delete the processes based on the specified statuses and the configured days
+ * from their creation.
+ */
+ private void performDeletion(Context context) throws SQLException, IOException, AuthorizeException {
+
+ List statuses = getProcessToDeleteStatuses();
+ Date creationDate = calculateCreationDate();
+
+ handler.logInfo("Searching for processes with status: " + statuses);
+ List processes = processService.findByStatusAndCreationTimeOlderThan(context, statuses, creationDate);
+ handler.logInfo("Found " + processes.size() + " processes to be deleted");
+ for (Process process : processes) {
+ processService.delete(context, process);
+ }
+
+ handler.logInfo("Process cleanup completed");
+
+ }
+
+ /**
+ * Returns the list of Process statuses do be deleted.
+ */
+ private List getProcessToDeleteStatuses() {
+ List statuses = new ArrayList();
+ if (cleanCompleted) {
+ statuses.add(ProcessStatus.COMPLETED);
+ }
+ if (cleanFailed) {
+ statuses.add(ProcessStatus.FAILED);
+ }
+ if (cleanRunning) {
+ statuses.add(ProcessStatus.RUNNING);
+ }
+ return statuses;
+ }
+
+ private Date calculateCreationDate() {
+ return DateUtils.addDays(new Date(), -days);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public ProcessCleanerConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager()
+ .getServiceByName("process-cleaner", ProcessCleanerConfiguration.class);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCli.java b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCli.java
new file mode 100644
index 000000000000..292c6c372e4f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCli.java
@@ -0,0 +1,18 @@
+/**
+ * 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.administer;
+
+/**
+ * The {@link ProcessCleaner} for CLI.
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class ProcessCleanerCli extends ProcessCleaner {
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCliConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCliConfiguration.java
new file mode 100644
index 000000000000..043990156d16
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerCliConfiguration.java
@@ -0,0 +1,18 @@
+/**
+ * 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.administer;
+
+/**
+ * The {@link ProcessCleanerConfiguration} for CLI.
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class ProcessCleanerCliConfiguration extends ProcessCleanerConfiguration {
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java
new file mode 100644
index 000000000000..91dcfb5dfec5
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java
@@ -0,0 +1,53 @@
+/**
+ * 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.administer;
+
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link ProcessCleaner} script.
+ */
+public class ProcessCleanerConfiguration extends ScriptConfiguration {
+
+ private Class dspaceRunnableClass;
+
+ @Override
+ public Options getOptions() {
+ if (options == null) {
+
+ Options options = new Options();
+
+ options.addOption("h", "help", false, "help");
+
+ options.addOption("r", "running", false, "delete the process with RUNNING status");
+ options.getOption("r").setType(boolean.class);
+
+ options.addOption("f", "failed", false, "delete the process with FAILED status");
+ options.getOption("f").setType(boolean.class);
+
+ options.addOption("c", "completed", false,
+ "delete the process with COMPLETED status (default if no statuses are specified)");
+ options.getOption("c").setType(boolean.class);
+
+ super.options = options;
+ }
+ return options;
+ }
+
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableClass;
+ }
+
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableClass = dspaceRunnableClass;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java
index 5b5f70412ac2..27a653421312 100644
--- a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java
+++ b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java
@@ -13,8 +13,11 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
-import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -72,9 +75,10 @@ public static Document loadXML(String filename)
* @throws TransformerException if error
*/
public static String getElementData(Node parentElement, String childName)
- throws TransformerException {
+ throws XPathExpressionException {
// Grab the child node
- Node childNode = XPathAPI.selectSingleNode(parentElement, childName);
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ Node childNode = (Node) xPath.compile(childName).evaluate(parentElement, XPathConstants.NODE);
if (childNode == null) {
// No child node, so no values
@@ -115,9 +119,10 @@ public static String getElementData(Node parentElement, String childName)
* @throws TransformerException if error
*/
public static String[] getRepeatedElementData(Node parentElement,
- String childName) throws TransformerException {
+ String childName) throws XPathExpressionException {
// Grab the child node
- NodeList childNodes = XPathAPI.selectNodeList(parentElement, childName);
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList childNodes = (NodeList) xPath.compile(childName).evaluate(parentElement, XPathConstants.NODESET);
String[] data = new String[childNodes.getLength()];
diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java
index 2b6a01b558df..bbf320a0d5e5 100644
--- a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java
+++ b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java
@@ -16,9 +16,12 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
import org.apache.logging.log4j.Logger;
-import org.apache.xpath.XPathAPI;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.factory.ContentServiceFactory;
@@ -122,12 +125,13 @@ public static void main(String[] argv) throws Exception {
*/
public static void loadBitstreamFormats(Context context, String filename)
throws SQLException, IOException, ParserConfigurationException,
- SAXException, TransformerException, AuthorizeException {
+ SAXException, TransformerException, AuthorizeException, XPathExpressionException {
Document document = loadXML(filename);
// Get the nodes corresponding to formats
- NodeList typeNodes = XPathAPI.selectNodeList(document,
- "dspace-bitstream-types/bitstream-type");
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList typeNodes = (NodeList) xPath.compile("dspace-bitstream-types/bitstream-type")
+ .evaluate(document, XPathConstants.NODESET);
// Add each one as a new format to the registry
for (int i = 0; i < typeNodes.getLength(); i++) {
@@ -151,8 +155,7 @@ public static void loadBitstreamFormats(Context context, String filename)
* @throws AuthorizeException if authorization error
*/
private static void loadFormat(Context context, Node node)
- throws SQLException, IOException, TransformerException,
- AuthorizeException {
+ throws SQLException, AuthorizeException, XPathExpressionException {
// Get the values
String mimeType = getElementData(node, "mimetype");
String shortDesc = getElementData(node, "short_description");
@@ -231,9 +234,10 @@ private static Document loadXML(String filename) throws IOException,
* @throws TransformerException if transformer error
*/
private static String getElementData(Node parentElement, String childName)
- throws TransformerException {
+ throws XPathExpressionException {
// Grab the child node
- Node childNode = XPathAPI.selectSingleNode(parentElement, childName);
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ Node childNode = (Node) xPath.compile(childName).evaluate(parentElement, XPathConstants.NODE);
if (childNode == null) {
// No child node, so no values
@@ -274,9 +278,10 @@ private static String getElementData(Node parentElement, String childName)
* @throws TransformerException if transformer error
*/
private static String[] getRepeatedElementData(Node parentElement,
- String childName) throws TransformerException {
+ String childName) throws XPathExpressionException {
// Grab the child node
- NodeList childNodes = XPathAPI.selectNodeList(parentElement, childName);
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList childNodes = (NodeList) xPath.compile(childName).evaluate(parentElement, XPathConstants.NODESET);
String[] data = new String[childNodes.getLength()];
diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
index 89d9ffe5a841..13a1b3b5bbf8 100644
--- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
+++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java
@@ -30,6 +30,10 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
@@ -38,7 +42,7 @@
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
-import org.apache.xpath.XPathAPI;
+import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Community;
@@ -52,9 +56,11 @@
import org.dspace.core.Context;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
-import org.jdom.Element;
-import org.jdom.output.Format;
-import org.jdom.output.XMLOutputter;
+import org.dspace.handle.factory.HandleServiceFactory;
+import org.dspace.handle.service.HandleService;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
@@ -76,6 +82,7 @@
*
*
* }
+ *
*
* It can be arbitrarily deep, and supports all the metadata elements
* that make up the community and collection metadata. See the system
@@ -104,12 +111,14 @@ public class StructBuilder {
*/
private static final Map communityMap = new HashMap<>();
- protected static CommunityService communityService
+ protected static final CommunityService communityService
= ContentServiceFactory.getInstance().getCommunityService();
- protected static CollectionService collectionService
+ protected static final CollectionService collectionService
= ContentServiceFactory.getInstance().getCollectionService();
- protected static EPersonService ePersonService
+ protected static final EPersonService ePersonService
= EPersonServiceFactory.getInstance().getEPersonService();
+ protected static final HandleService handleService
+ = HandleServiceFactory.getInstance().getHandleService();
/**
* Default constructor
@@ -135,16 +144,18 @@ private StructBuilder() { }
* @throws SQLException passed through.
* @throws FileNotFoundException if input or output could not be opened.
* @throws TransformerException if the input document is invalid.
+ * @throws XPathExpressionException passed through.
*/
public static void main(String[] argv)
- throws ParserConfigurationException, SQLException,
- FileNotFoundException, IOException, TransformerException {
+ throws ParserConfigurationException, SQLException,
+ IOException, TransformerException, XPathExpressionException {
// Define command line options.
Options options = new Options();
options.addOption("h", "help", false, "Print this help message.");
options.addOption("?", "help");
options.addOption("x", "export", false, "Export the current structure as XML.");
+ options.addOption("k", "keep-handles", false, "Apply Handles from input document.");
options.addOption(Option.builder("e").longOpt("eperson")
.desc("User who is manipulating the repository's structure.")
@@ -206,6 +217,7 @@ public static void main(String[] argv)
// Export? Import?
if (line.hasOption('x')) { // export
exportStructure(context, outputStream);
+ outputStream.close();
} else { // Must be import
String input = line.getOptionValue('f');
if (null == input) {
@@ -220,7 +232,12 @@ public static void main(String[] argv)
inputStream = new FileInputStream(input);
}
- importStructure(context, inputStream, outputStream);
+ boolean keepHandles = options.hasOption("k");
+ importStructure(context, inputStream, outputStream, keepHandles);
+
+ inputStream.close();
+ outputStream.close();
+
// save changes from import
context.complete();
}
@@ -233,14 +250,17 @@ public static void main(String[] argv)
* @param context
* @param input XML which describes the new communities and collections.
* @param output input, annotated with the new objects' identifiers.
+ * @param keepHandles true if Handles should be set from input.
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws TransformerException
* @throws SQLException
*/
- static void importStructure(Context context, InputStream input, OutputStream output)
- throws IOException, ParserConfigurationException, SQLException, TransformerException {
+ static void importStructure(Context context, InputStream input,
+ OutputStream output, boolean keepHandles)
+ throws IOException, ParserConfigurationException, SQLException,
+ TransformerException, XPathExpressionException {
// load the XML
Document document = null;
@@ -258,15 +278,29 @@ static void importStructure(Context context, InputStream input, OutputStream out
// is properly structured.
try {
validate(document);
- } catch (TransformerException ex) {
+ } catch (XPathExpressionException ex) {
System.err.format("The input document is invalid: %s%n", ex.getMessage());
System.exit(1);
}
// Check for 'identifier' attributes -- possibly output by this class.
- NodeList identifierNodes = XPathAPI.selectNodeList(document, "//*[@identifier]");
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList identifierNodes = (NodeList) xPath.compile("//*[@identifier]")
+ .evaluate(document, XPathConstants.NODESET);
if (identifierNodes.getLength() > 0) {
- System.err.println("The input document has 'identifier' attributes, which will be ignored.");
+ if (!keepHandles) {
+ System.err.println("The input document has 'identifier' attributes, which will be ignored.");
+ } else {
+ for (int i = 0; i < identifierNodes.getLength() ; i++) {
+ String identifier = identifierNodes.item(i).getAttributes().item(0).getTextContent();
+ if (handleService.resolveToURL(context, identifier) != null) {
+ System.err.printf("The input document contains handle %s,"
+ + " which is in use already. Aborting...%n",
+ identifier);
+ System.exit(1);
+ }
+ }
+ }
}
// load the mappings into the member variable hashmaps
@@ -287,10 +321,11 @@ static void importStructure(Context context, InputStream input, OutputStream out
Element[] elements = new Element[]{};
try {
// get the top level community list
- NodeList first = XPathAPI.selectNodeList(document, "/import_structure/community");
+ NodeList first = (NodeList) xPath.compile("/import_structure/community")
+ .evaluate(document, XPathConstants.NODESET);
// run the import starting with the top level communities
- elements = handleCommunities(context, first, null);
+ elements = handleCommunities(context, first, null, keepHandles);
} catch (TransformerException ex) {
System.err.format("Input content not understood: %s%n", ex.getMessage());
System.exit(1);
@@ -307,7 +342,7 @@ static void importStructure(Context context, InputStream input, OutputStream out
}
// finally write the string into the output file.
- final org.jdom.Document xmlOutput = new org.jdom.Document(root);
+ final org.jdom2.Document xmlOutput = new org.jdom2.Document(root);
try {
new XMLOutputter().output(xmlOutput, output);
} catch (IOException e) {
@@ -411,7 +446,7 @@ static void exportStructure(Context context, OutputStream output) {
}
// Now write the structure out.
- org.jdom.Document xmlOutput = new org.jdom.Document(rootElement);
+ org.jdom2.Document xmlOutput = new org.jdom2.Document(rootElement);
try {
XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat());
outputter.output(xmlOutput, output);
@@ -456,14 +491,16 @@ private static void giveHelp(Options options) {
* @throws TransformerException if transformer error
*/
private static void validate(org.w3c.dom.Document document)
- throws TransformerException {
+ throws XPathExpressionException {
StringBuilder err = new StringBuilder();
boolean trip = false;
err.append("The following errors were encountered parsing the source XML.\n");
err.append("No changes have been made to the DSpace instance.\n\n");
- NodeList first = XPathAPI.selectNodeList(document, "/import_structure/community");
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList first = (NodeList) xPath.compile("/import_structure/community")
+ .evaluate(document, XPathConstants.NODESET);
if (first.getLength() == 0) {
err.append("-There are no top level communities in the source document.");
System.out.println(err.toString());
@@ -493,14 +530,15 @@ private static void validate(org.w3c.dom.Document document)
* no errors.
*/
private static String validateCommunities(NodeList communities, int level)
- throws TransformerException {
+ throws XPathExpressionException {
StringBuilder err = new StringBuilder();
boolean trip = false;
String errs = null;
+ XPath xPath = XPathFactory.newInstance().newXPath();
for (int i = 0; i < communities.getLength(); i++) {
Node n = communities.item(i);
- NodeList name = XPathAPI.selectNodeList(n, "name");
+ NodeList name = (NodeList) xPath.compile("name").evaluate(n, XPathConstants.NODESET);
if (name.getLength() != 1) {
String pos = Integer.toString(i + 1);
err.append("-The level ").append(level)
@@ -510,7 +548,7 @@ private static String validateCommunities(NodeList communities, int level)
}
// validate sub communities
- NodeList subCommunities = XPathAPI.selectNodeList(n, "community");
+ NodeList subCommunities = (NodeList) xPath.compile("community").evaluate(n, XPathConstants.NODESET);
String comErrs = validateCommunities(subCommunities, level + 1);
if (comErrs != null) {
err.append(comErrs);
@@ -518,7 +556,7 @@ private static String validateCommunities(NodeList communities, int level)
}
// validate collections
- NodeList collections = XPathAPI.selectNodeList(n, "collection");
+ NodeList collections = (NodeList) xPath.compile("collection").evaluate(n, XPathConstants.NODESET);
String colErrs = validateCollections(collections, level + 1);
if (colErrs != null) {
err.append(colErrs);
@@ -542,14 +580,15 @@ private static String validateCommunities(NodeList communities, int level)
* @return the errors to be generated by the calling method, or null if none
*/
private static String validateCollections(NodeList collections, int level)
- throws TransformerException {
+ throws XPathExpressionException {
StringBuilder err = new StringBuilder();
boolean trip = false;
String errs = null;
+ XPath xPath = XPathFactory.newInstance().newXPath();
for (int i = 0; i < collections.getLength(); i++) {
Node n = collections.item(i);
- NodeList name = XPathAPI.selectNodeList(n, "name");
+ NodeList name = (NodeList) xPath.compile("name").evaluate(n, XPathConstants.NODESET);
if (name.getLength() != 1) {
String pos = Integer.toString(i + 1);
err.append("-The level ").append(level)
@@ -609,22 +648,29 @@ private static String getStringValue(Node node) {
* @param context the context of the request
* @param communities a nodelist of communities to create along with their sub-structures
* @param parent the parent community of the nodelist of communities to create
+ * @param keepHandles use Handles from input.
* @return an element array containing additional information regarding the
* created communities (e.g. the handles they have been assigned)
*/
- private static Element[] handleCommunities(Context context, NodeList communities, Community parent)
- throws TransformerException, SQLException, AuthorizeException {
+ private static Element[] handleCommunities(Context context, NodeList communities,
+ Community parent, boolean keepHandles)
+ throws TransformerException, SQLException, AuthorizeException,
+ XPathExpressionException {
Element[] elements = new Element[communities.getLength()];
+ XPath xPath = XPathFactory.newInstance().newXPath();
for (int i = 0; i < communities.getLength(); i++) {
- Community community;
- Element element = new Element("community");
+ Node tn = communities.item(i);
+ Node identifier = tn.getAttributes().getNamedItem("identifier");
// create the community or sub community
- if (parent != null) {
+ Community community;
+ if (null == identifier
+ || StringUtils.isBlank(identifier.getNodeValue())
+ || !keepHandles) {
community = communityService.create(parent, context);
} else {
- community = communityService.create(null, context);
+ community = communityService.create(parent, context, identifier.getNodeValue());
}
// default the short description to be an empty string
@@ -632,9 +678,8 @@ private static Element[] handleCommunities(Context context, NodeList communities
MD_SHORT_DESCRIPTION, null, " ");
// now update the metadata
- Node tn = communities.item(i);
for (Map.Entry entry : communityMap.entrySet()) {
- NodeList nl = XPathAPI.selectNodeList(tn, entry.getKey());
+ NodeList nl = (NodeList) xPath.compile(entry.getKey()).evaluate(tn, XPathConstants.NODESET);
if (nl.getLength() == 1) {
communityService.setMetadataSingleValue(context, community,
entry.getValue(), null, getStringValue(nl.item(0)));
@@ -658,6 +703,7 @@ private static Element[] handleCommunities(Context context, NodeList communities
// but it's here to keep it separate from the create process in
// case
// we want to move it or make it switchable later
+ Element element = new Element("community");
element.setAttribute("identifier", community.getHandle());
Element nameElement = new Element("name");
@@ -700,12 +746,16 @@ private static Element[] handleCommunities(Context context, NodeList communities
}
// handle sub communities
- NodeList subCommunities = XPathAPI.selectNodeList(tn, "community");
- Element[] subCommunityElements = handleCommunities(context, subCommunities, community);
+ NodeList subCommunities = (NodeList) xPath.compile("community")
+ .evaluate(tn, XPathConstants.NODESET);
+ Element[] subCommunityElements = handleCommunities(context,
+ subCommunities, community, keepHandles);
// handle collections
- NodeList collections = XPathAPI.selectNodeList(tn, "collection");
- Element[] collectionElements = handleCollections(context, collections, community);
+ NodeList collections = (NodeList) xPath.compile("collection")
+ .evaluate(tn, XPathConstants.NODESET);
+ Element[] collectionElements = handleCollections(context,
+ collections, community, keepHandles);
int j;
for (j = 0; j < subCommunityElements.length; j++) {
@@ -730,22 +780,33 @@ private static Element[] handleCommunities(Context context, NodeList communities
* @return an Element array containing additional information about the
* created collections (e.g. the handle)
*/
- private static Element[] handleCollections(Context context, NodeList collections, Community parent)
- throws TransformerException, SQLException, AuthorizeException {
+ private static Element[] handleCollections(Context context,
+ NodeList collections, Community parent, boolean keepHandles)
+ throws SQLException, AuthorizeException, XPathExpressionException {
Element[] elements = new Element[collections.getLength()];
+ XPath xPath = XPathFactory.newInstance().newXPath();
for (int i = 0; i < collections.getLength(); i++) {
- Element element = new Element("collection");
- Collection collection = collectionService.create(context, parent);
+ Node tn = collections.item(i);
+ Node identifier = tn.getAttributes().getNamedItem("identifier");
+
+ // Create the Collection.
+ Collection collection;
+ if (null == identifier
+ || StringUtils.isBlank(identifier.getNodeValue())
+ || !keepHandles) {
+ collection = collectionService.create(context, parent);
+ } else {
+ collection = collectionService.create(context, parent, identifier.getNodeValue());
+ }
// default the short description to the empty string
collectionService.setMetadataSingleValue(context, collection,
MD_SHORT_DESCRIPTION, Item.ANY, " ");
// import the rest of the metadata
- Node tn = collections.item(i);
for (Map.Entry entry : collectionMap.entrySet()) {
- NodeList nl = XPathAPI.selectNodeList(tn, entry.getKey());
+ NodeList nl = (NodeList) xPath.compile(entry.getKey()).evaluate(tn, XPathConstants.NODESET);
if (nl.getLength() == 1) {
collectionService.setMetadataSingleValue(context, collection,
entry.getValue(), null, getStringValue(nl.item(0)));
@@ -754,6 +815,7 @@ private static Element[] handleCollections(Context context, NodeList collections
collectionService.update(context, collection);
+ Element element = new Element("collection");
element.setAttribute("identifier", collection.getHandle());
Element nameElement = new Element("name");
diff --git a/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java b/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java
new file mode 100644
index 000000000000..a200cab8781f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java
@@ -0,0 +1,54 @@
+/**
+ * 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.alerts;
+
+/**
+ * Enum representing the options for allowing sessions:
+ * ALLOW_ALL_SESSIONS - Will allow all users to log in and continue their sessions
+ * ALLOW_CURRENT_SESSIONS_ONLY - Will prevent non admin users from logging in, however logged-in users
+ * will remain logged in
+ * ALLOW_ADMIN_SESSIONS_ONLY - Only admin users can log in, non admin sessions will be interrupted
+ *
+ * NOTE: This functionality can be stored in the database, but no support is present right now to interrupt and prevent
+ * sessions.
+ */
+public enum AllowSessionsEnum {
+ ALLOW_ALL_SESSIONS("all"),
+ ALLOW_CURRENT_SESSIONS_ONLY("current"),
+ ALLOW_ADMIN_SESSIONS_ONLY("admin");
+
+ private String allowSessionsType;
+
+ AllowSessionsEnum(String allowSessionsType) {
+ this.allowSessionsType = allowSessionsType;
+ }
+
+ public String getValue() {
+ return allowSessionsType;
+ }
+
+ public static AllowSessionsEnum fromString(String alertAllowSessionType) {
+ if (alertAllowSessionType == null) {
+ return AllowSessionsEnum.ALLOW_ALL_SESSIONS;
+ }
+
+ switch (alertAllowSessionType) {
+ case "all":
+ return AllowSessionsEnum.ALLOW_ALL_SESSIONS;
+ case "current":
+ return AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY;
+ case "admin" :
+ return AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY;
+ default:
+ throw new IllegalArgumentException("No corresponding enum value for provided string: "
+ + alertAllowSessionType);
+ }
+ }
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java
new file mode 100644
index 000000000000..f56cbdcce9e9
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java
@@ -0,0 +1,179 @@
+/**
+ * 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.alerts;
+
+import java.util.Date;
+import javax.persistence.Cacheable;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.dspace.core.ReloadableEntity;
+import org.hibernate.annotations.CacheConcurrencyStrategy;
+
+/**
+ * Database object representing system-wide alerts
+ */
+@Entity
+@Cacheable
+@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, include = "non-lazy")
+@Table(name = "systemwidealert")
+public class SystemWideAlert implements ReloadableEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "alert_id_seq")
+ @SequenceGenerator(name = "alert_id_seq", sequenceName = "alert_id_seq", allocationSize = 1)
+ @Column(name = "alert_id", unique = true, nullable = false)
+ private Integer alertId;
+
+ @Column(name = "message", nullable = false)
+ private String message;
+
+ @Column(name = "allow_sessions")
+ private String allowSessions;
+
+ @Column(name = "countdown_to")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date countdownTo;
+
+ @Column(name = "active")
+ private boolean active;
+
+ protected SystemWideAlert() {
+ }
+
+ /**
+ * This method returns the ID that the system-wide alert holds within the database
+ *
+ * @return The ID that the system-wide alert holds within the database
+ */
+ @Override
+ public Integer getID() {
+ return alertId;
+ }
+
+ /**
+ * Set the ID for the system-wide alert
+ *
+ * @param alertID The ID to set
+ */
+ public void setID(final Integer alertID) {
+ this.alertId = alertID;
+ }
+
+ /**
+ * Retrieve the message of the system-wide alert
+ *
+ * @return the message of the system-wide alert
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Set the message of the system-wide alert
+ *
+ * @param message The message to set
+ */
+ public void setMessage(final String message) {
+ this.message = message;
+ }
+
+ /**
+ * Retrieve what kind of sessions are allowed while the system-wide alert is active
+ *
+ * @return what kind of sessions are allowed while the system-wide alert is active
+ */
+ public AllowSessionsEnum getAllowSessions() {
+ return AllowSessionsEnum.fromString(allowSessions);
+ }
+
+ /**
+ * Set what kind of sessions are allowed while the system-wide alert is active
+ *
+ * @param allowSessions Integer representing what kind of sessions are allowed
+ */
+ public void setAllowSessions(AllowSessionsEnum allowSessions) {
+ this.allowSessions = allowSessions.getValue();
+ }
+
+ /**
+ * Retrieve the date to which will be count down when the system-wide alert is active
+ *
+ * @return the date to which will be count down when the system-wide alert is active
+ */
+ public Date getCountdownTo() {
+ return countdownTo;
+ }
+
+ /**
+ * Set the date to which will be count down when the system-wide alert is active
+ *
+ * @param countdownTo The date to which will be count down
+ */
+ public void setCountdownTo(final Date countdownTo) {
+ this.countdownTo = countdownTo;
+ }
+
+ /**
+ * Retrieve whether the system-wide alert is active
+ *
+ * @return whether the system-wide alert is active
+ */
+ public boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Set whether the system-wide alert is active
+ *
+ * @param active Whether the system-wide alert is active
+ */
+ public void setActive(final boolean active) {
+ this.active = active;
+ }
+
+ /**
+ * Return true
if other
is the same SystemWideAlert
+ * as this object, false
otherwise
+ *
+ * @param other object to compare to
+ * @return true
if object passed in represents the same
+ * system-wide alert as this object
+ */
+ @Override
+ public boolean equals(Object other) {
+ return (other instanceof SystemWideAlert &&
+ new EqualsBuilder().append(this.getID(), ((SystemWideAlert) other).getID())
+ .append(this.getMessage(), ((SystemWideAlert) other).getMessage())
+ .append(this.getAllowSessions(), ((SystemWideAlert) other).getAllowSessions())
+ .append(this.getCountdownTo(), ((SystemWideAlert) other).getCountdownTo())
+ .append(this.isActive(), ((SystemWideAlert) other).isActive())
+ .isEquals());
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.getID())
+ .append(this.getMessage())
+ .append(this.getAllowSessions())
+ .append(this.getCountdownTo())
+ .append(this.isActive())
+ .toHashCode();
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java
new file mode 100644
index 000000000000..9ddf6c97d111
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java
@@ -0,0 +1,129 @@
+/**
+ * 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.alerts;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.logging.log4j.Logger;
+import org.dspace.alerts.dao.SystemWideAlertDAO;
+import org.dspace.alerts.service.SystemWideAlertService;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.service.AuthorizeService;
+import org.dspace.core.Context;
+import org.dspace.core.LogHelper;
+import org.dspace.eperson.EPerson;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * The implementation for the {@link SystemWideAlertService} class
+ */
+public class SystemWideAlertServiceImpl implements SystemWideAlertService {
+
+ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SystemWideAlertService.class);
+
+
+ @Autowired
+ private SystemWideAlertDAO systemWideAlertDAO;
+
+ @Autowired
+ private AuthorizeService authorizeService;
+
+ @Override
+ public SystemWideAlert create(final Context context, final String message,
+ final AllowSessionsEnum allowSessionsType,
+ final Date countdownTo, final boolean active) throws SQLException,
+ AuthorizeException {
+ if (!authorizeService.isAdmin(context)) {
+ throw new AuthorizeException(
+ "Only administrators can create a system-wide alert");
+ }
+ SystemWideAlert systemWideAlert = new SystemWideAlert();
+ systemWideAlert.setMessage(message);
+ systemWideAlert.setAllowSessions(allowSessionsType);
+ systemWideAlert.setCountdownTo(countdownTo);
+ systemWideAlert.setActive(active);
+
+ SystemWideAlert createdAlert = systemWideAlertDAO.create(context, systemWideAlert);
+ log.info(LogHelper.getHeader(context, "system_wide_alert_create",
+ "System Wide Alert has been created with message: '" + message + "' and ID "
+ + createdAlert.getID() + " and allowSessionsType " + allowSessionsType +
+ " and active set to " + active));
+
+
+ return createdAlert;
+ }
+
+ @Override
+ public SystemWideAlert find(final Context context, final int alertId) throws SQLException {
+ return systemWideAlertDAO.findByID(context, SystemWideAlert.class, alertId);
+ }
+
+ @Override
+ public List findAll(final Context context) throws SQLException {
+ return systemWideAlertDAO.findAll(context, SystemWideAlert.class);
+ }
+
+ @Override
+ public List findAll(final Context context, final int limit, final int offset) throws SQLException {
+ return systemWideAlertDAO.findAll(context, limit, offset);
+ }
+
+ @Override
+ public List findAllActive(final Context context, final int limit, final int offset)
+ throws SQLException {
+ return systemWideAlertDAO.findAllActive(context, limit, offset);
+ }
+
+ @Override
+ public void delete(final Context context, final SystemWideAlert systemWideAlert)
+ throws SQLException, IOException, AuthorizeException {
+ if (!authorizeService.isAdmin(context)) {
+ throw new AuthorizeException(
+ "Only administrators can create a system-wide alert");
+ }
+ systemWideAlertDAO.delete(context, systemWideAlert);
+ log.info(LogHelper.getHeader(context, "system_wide_alert_create",
+ "System Wide Alert with ID " + systemWideAlert.getID() + " has been deleted"));
+
+ }
+
+ @Override
+ public void update(final Context context, final SystemWideAlert systemWideAlert)
+ throws SQLException, AuthorizeException {
+ if (!authorizeService.isAdmin(context)) {
+ throw new AuthorizeException(
+ "Only administrators can create a system-wide alert");
+ }
+ systemWideAlertDAO.save(context, systemWideAlert);
+
+ }
+
+ @Override
+ public boolean canNonAdminUserLogin(Context context) throws SQLException {
+ List active = findAllActive(context, 1, 0);
+ if (active == null || active.isEmpty()) {
+ return true;
+ }
+ return active.get(0).getAllowSessions() == AllowSessionsEnum.ALLOW_ALL_SESSIONS;
+ }
+
+ @Override
+ public boolean canUserMaintainSession(Context context, EPerson ePerson) throws SQLException {
+ if (authorizeService.isAdmin(context, ePerson)) {
+ return true;
+ }
+ List active = findAllActive(context, 1, 0);
+ if (active == null || active.isEmpty()) {
+ return true;
+ }
+ return active.get(0).getAllowSessions() != AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java b/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java
new file mode 100644
index 000000000000..b26b64758355
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java
@@ -0,0 +1,45 @@
+/**
+ * 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.alerts.dao;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.dspace.alerts.SystemWideAlert;
+import org.dspace.core.Context;
+import org.dspace.core.GenericDAO;
+
+/**
+ * This is the Data Access Object for the {@link SystemWideAlert} object
+ */
+public interface SystemWideAlertDAO extends GenericDAO {
+
+ /**
+ * Returns a list of all SystemWideAlert objects in the database
+ *
+ * @param context The relevant DSpace context
+ * @param limit The limit for the amount of SystemWideAlerts returned
+ * @param offset The offset for the Processes to be returned
+ * @return The list of all SystemWideAlert objects in the Database
+ * @throws SQLException If something goes wrong
+ */
+ List findAll(Context context, int limit, int offset) throws SQLException;
+
+ /**
+ * Returns a list of all active SystemWideAlert objects in the database
+ *
+ * @param context The relevant DSpace context
+ * @param limit The limit for the amount of SystemWideAlerts returned
+ * @param offset The offset for the Processes to be returned
+ * @return The list of all SystemWideAlert objects in the Database
+ * @throws SQLException If something goes wrong
+ */
+ List findAllActive(Context context, int limit, int offset) throws SQLException;
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java
new file mode 100644
index 000000000000..13a0e0af236a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java
@@ -0,0 +1,48 @@
+/**
+ * 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.alerts.dao.impl;
+
+import java.sql.SQLException;
+import java.util.List;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Root;
+
+import org.dspace.alerts.SystemWideAlert;
+import org.dspace.alerts.SystemWideAlert_;
+import org.dspace.alerts.dao.SystemWideAlertDAO;
+import org.dspace.core.AbstractHibernateDAO;
+import org.dspace.core.Context;
+
+/**
+ * Implementation class for the {@link SystemWideAlertDAO}
+ */
+public class SystemWideAlertDAOImpl extends AbstractHibernateDAO implements SystemWideAlertDAO {
+
+ public List findAll(final Context context, final int limit, final int offset) throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, SystemWideAlert.class);
+ Root alertRoot = criteriaQuery.from(SystemWideAlert.class);
+ criteriaQuery.select(alertRoot);
+
+ return list(context, criteriaQuery, false, SystemWideAlert.class, limit, offset);
+ }
+
+ public List findAllActive(final Context context, final int limit, final int offset)
+ throws SQLException {
+ CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
+ CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, SystemWideAlert.class);
+ Root alertRoot = criteriaQuery.from(SystemWideAlert.class);
+ criteriaQuery.select(alertRoot);
+ criteriaQuery.where(criteriaBuilder.equal(alertRoot.get(SystemWideAlert_.active), true));
+
+ return list(context, criteriaQuery, false, SystemWideAlert.class, limit, offset);
+ }
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java b/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java
new file mode 100644
index 000000000000..cf231308849d
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java
@@ -0,0 +1,118 @@
+/**
+ * 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.alerts.service;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+import org.dspace.alerts.AllowSessionsEnum;
+import org.dspace.alerts.SystemWideAlert;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+
+/**
+ * An interface for the SystemWideAlertService with methods regarding the SystemWideAlert workload
+ */
+public interface SystemWideAlertService {
+
+ /**
+ * This method will create a SystemWideAlert object in the database
+ *
+ * @param context The relevant DSpace context
+ * @param message The message of the system-wide alert
+ * @param allowSessionsType Which sessions need to be allowed for the system-wide alert
+ * @param countdownTo The date to which to count down to when the system-wide alert is active
+ * @param active Whether the system-wide alert os active
+ * @return The created SystemWideAlert object
+ * @throws SQLException If something goes wrong
+ */
+ SystemWideAlert create(Context context, String message, AllowSessionsEnum allowSessionsType,
+ Date countdownTo, boolean active
+ ) throws SQLException, AuthorizeException;
+
+ /**
+ * This method will retrieve a SystemWideAlert object from the Database with the given ID
+ *
+ * @param context The relevant DSpace context
+ * @param alertId The alert id on which we'll search for in the database
+ * @return The system-wide alert that holds the given alert id
+ * @throws SQLException If something goes wrong
+ */
+ SystemWideAlert find(Context context, int alertId) throws SQLException;
+
+ /**
+ * Returns a list of all SystemWideAlert objects in the database
+ *
+ * @param context The relevant DSpace context
+ * @return The list of all SystemWideAlert objects in the Database
+ * @throws SQLException If something goes wrong
+ */
+ List findAll(Context context) throws SQLException;
+
+ /**
+ * Returns a list of all SystemWideAlert objects in the database
+ *
+ * @param context The relevant DSpace context
+ * @param limit The limit for the amount of system-wide alerts returned
+ * @param offset The offset for the system-wide alerts to be returned
+ * @return The list of all SystemWideAlert objects in the Database
+ * @throws SQLException If something goes wrong
+ */
+ List findAll(Context context, int limit, int offset) throws SQLException;
+
+
+ /**
+ * Returns a list of all active SystemWideAlert objects in the database
+ *
+ * @param context The relevant DSpace context
+ * @return The list of all active SystemWideAlert objects in the database
+ * @throws SQLException If something goes wrong
+ */
+ List findAllActive(Context context, int limit, int offset) throws SQLException;
+
+ /**
+ * This method will delete the given SystemWideAlert object from the database
+ *
+ * @param context The relevant DSpace context
+ * @param systemWideAlert The SystemWideAlert object to be deleted
+ * @throws SQLException If something goes wrong
+ */
+ void delete(Context context, SystemWideAlert systemWideAlert)
+ throws SQLException, IOException, AuthorizeException;
+
+
+ /**
+ * This method will be used to update the given SystemWideAlert object in the database
+ *
+ * @param context The relevant DSpace context
+ * @param systemWideAlert The SystemWideAlert object to be updated
+ * @throws SQLException If something goes wrong
+ */
+ void update(Context context, SystemWideAlert systemWideAlert) throws SQLException, AuthorizeException;
+
+
+ /**
+ * Verifies if the user connected to the current context can retain its session
+ *
+ * @param context The relevant DSpace context
+ * @return if the user connected to the current context can retain its session
+ */
+ boolean canUserMaintainSession(Context context, EPerson ePerson) throws SQLException;
+
+
+ /**
+ * Verifies if a non admin user can log in
+ *
+ * @param context The relevant DSpace context
+ * @return if a non admin user can log in
+ */
+ boolean canNonAdminUserLogin(Context context) throws SQLException;
+}
diff --git a/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java b/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java
new file mode 100644
index 000000000000..e0231154b3b9
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/api/DSpaceApi.java
@@ -0,0 +1,116 @@
+/**
+ * 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/
+ */
+
+
+/* Created for LINDAT/CLARIAH-CZ (UFAL) */
+
+package org.dspace.api;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.dspace.content.DSpaceObject;
+import org.dspace.handle.HandlePlugin;
+import org.dspace.handle.PIDService;
+import org.dspace.services.ConfigurationService;
+import org.dspace.utils.DSpace;
+
+public class DSpaceApi {
+
+ private static final org.apache.logging.log4j.Logger log = LogManager.getLogger();
+
+ private static ConfigurationService configurationService = new DSpace().getConfigurationService();
+
+ private DSpaceApi() {
+
+ }
+ /**
+ * Create a new handle PID. This is modified implementation for UFAL, using
+ * the PID service pidconsortium.eu as wrapped in the PIDService class.
+ *
+ * Note: this function creates a handle to a provisional existing URL and
+ * the handle must be updated to point to the final URL once DSpace is able
+ * to report the URL exists (otherwise the pidservice will refuse to set the
+ * URL)
+ *
+ * @return A new handle PID
+ * @exception Exception If error occurrs
+ */
+ public static String handle_HandleManager_createId(Logger log, Long id,
+ String prefix, String suffix) throws IOException {
+
+ /* Modified by PP for use pidconsortium.eu at UFAL/CLARIN */
+
+ String base_url = configurationService.getProperty("dspace.server.url") + "?dummy=" + id;
+
+ /* OK check whether this url has not received pid earlier */
+ //This should usually return null (404)
+ String handle = null;
+ try {
+ handle = PIDService.findHandle(base_url, prefix);
+ } catch (Exception e) {
+ log.error("Error finding handle: " + e);
+ }
+ //if not then log and reuse - this is a dummy url, those should not be seen anywhere
+ if (handle != null) {
+ log.warn("Url [" + base_url + "] already has PID(s) (" + handle + ").");
+ return handle;
+ }
+ /* /OK/ */
+
+ log.debug("Asking for a new PID using a dummy URL " + base_url);
+
+ /* request a new PID, initially pointing to dspace base_uri+id */
+ String pid = null;
+ try {
+ if (suffix != null && !suffix.isEmpty() && PIDService.supportsCustomPIDs()) {
+ pid = PIDService.createCustomPID(base_url, prefix, suffix);
+ } else {
+ pid = PIDService.createPID(base_url, prefix);
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+
+ log.debug("got PID " + pid);
+ return pid;
+ }
+
+ /**
+ * Modify an existing PID to point to the corresponding DSpace handle
+ *
+ * @exception SQLException If a database error occurs
+ */
+ public static void handle_HandleManager_registerFinalHandleURL(Logger log,
+ String pid, DSpaceObject dso) throws IOException {
+ if (pid == null) {
+ log.info("Modification failed invalid/null PID.");
+ return;
+ }
+
+ String url = configurationService.getProperty("dspace.url");
+ url = url + (url.endsWith("/") ? "" : "/") + "handle/" + pid;
+
+ /*
+ * request modification of the PID to point to the correct URL, which
+ * itself should contain the PID as a substring
+ */
+ log.debug("Asking for changing the PID '" + pid + "' to " + url);
+
+ try {
+ Map fields = HandlePlugin.extractMetadata(dso);
+ PIDService.modifyPID(pid, url, fields);
+ } catch (Exception e) {
+ throw new IOException("Failed to map PID " + pid + " to " + url
+ + " (" + e.toString() + ")");
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java
new file mode 100644
index 000000000000..7bef232f0450
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java
@@ -0,0 +1,689 @@
+/**
+ * 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.bulkaccesscontrol;
+
+import static org.apache.commons.collections4.CollectionUtils.isEmpty;
+import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
+import static org.dspace.authorize.ResourcePolicy.TYPE_CUSTOM;
+import static org.dspace.authorize.ResourcePolicy.TYPE_INHERITED;
+import static org.dspace.core.Constants.CONTENT_BUNDLE_NAME;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.SQLException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.TimeZone;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.bulkaccesscontrol.exception.BulkAccessControlException;
+import org.dspace.app.bulkaccesscontrol.model.AccessCondition;
+import org.dspace.app.bulkaccesscontrol.model.AccessConditionBitstream;
+import org.dspace.app.bulkaccesscontrol.model.AccessConditionItem;
+import org.dspace.app.bulkaccesscontrol.model.BulkAccessConditionConfiguration;
+import org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput;
+import org.dspace.app.bulkaccesscontrol.service.BulkAccessConditionConfigurationService;
+import org.dspace.app.mediafilter.factory.MediaFilterServiceFactory;
+import org.dspace.app.mediafilter.service.MediaFilterService;
+import org.dspace.app.util.DSpaceObjectUtilsImpl;
+import org.dspace.app.util.service.DSpaceObjectUtils;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.authorize.factory.AuthorizeServiceFactory;
+import org.dspace.authorize.service.ResourcePolicyService;
+import org.dspace.content.Bitstream;
+import org.dspace.content.Collection;
+import org.dspace.content.DSpaceObject;
+import org.dspace.content.Item;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.discovery.DiscoverQuery;
+import org.dspace.discovery.SearchService;
+import org.dspace.discovery.SearchServiceException;
+import org.dspace.discovery.SearchUtils;
+import org.dspace.discovery.indexobject.IndexableItem;
+import org.dspace.eperson.EPerson;
+import org.dspace.eperson.factory.EPersonServiceFactory;
+import org.dspace.eperson.service.EPersonService;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.services.ConfigurationService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+import org.dspace.submit.model.AccessConditionOption;
+import org.dspace.utils.DSpace;
+
+/**
+ * Implementation of {@link DSpaceRunnable} to perform a bulk access control via json file.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ *
+ */
+public class BulkAccessControl extends DSpaceRunnable> {
+
+ private DSpaceObjectUtils dSpaceObjectUtils;
+
+ private SearchService searchService;
+
+ private ItemService itemService;
+
+ private String filename;
+
+ private List uuids;
+
+ private Context context;
+
+ private BulkAccessConditionConfigurationService bulkAccessConditionConfigurationService;
+
+ private ResourcePolicyService resourcePolicyService;
+
+ protected EPersonService epersonService;
+
+ private ConfigurationService configurationService;
+
+ private MediaFilterService mediaFilterService;
+
+ private Map itemAccessConditions;
+
+ private Map uploadAccessConditions;
+
+ private final String ADD_MODE = "add";
+
+ private final String REPLACE_MODE = "replace";
+
+ private boolean help = false;
+
+ protected String eperson = null;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setup() throws ParseException {
+
+ this.searchService = SearchUtils.getSearchService();
+ this.itemService = ContentServiceFactory.getInstance().getItemService();
+ this.resourcePolicyService = AuthorizeServiceFactory.getInstance().getResourcePolicyService();
+ this.epersonService = EPersonServiceFactory.getInstance().getEPersonService();
+ this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
+ mediaFilterService = MediaFilterServiceFactory.getInstance().getMediaFilterService();
+ mediaFilterService.setLogHandler(handler);
+ this.bulkAccessConditionConfigurationService = new DSpace().getServiceManager().getServiceByName(
+ "bulkAccessConditionConfigurationService", BulkAccessConditionConfigurationService.class);
+ this.dSpaceObjectUtils = new DSpace().getServiceManager().getServiceByName(
+ DSpaceObjectUtilsImpl.class.getName(), DSpaceObjectUtilsImpl.class);
+
+ BulkAccessConditionConfiguration bulkAccessConditionConfiguration =
+ bulkAccessConditionConfigurationService.getBulkAccessConditionConfiguration("default");
+
+ itemAccessConditions = bulkAccessConditionConfiguration
+ .getItemAccessConditionOptions()
+ .stream()
+ .collect(Collectors.toMap(AccessConditionOption::getName, Function.identity()));
+
+ uploadAccessConditions = bulkAccessConditionConfiguration
+ .getBitstreamAccessConditionOptions()
+ .stream()
+ .collect(Collectors.toMap(AccessConditionOption::getName, Function.identity()));
+
+ help = commandLine.hasOption('h');
+ filename = commandLine.getOptionValue('f');
+ uuids = commandLine.hasOption('u') ? Arrays.asList(commandLine.getOptionValues('u')) : null;
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+
+ if (help) {
+ printHelp();
+ return;
+ }
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setTimeZone(TimeZone.getTimeZone("UTC"));
+ BulkAccessControlInput accessControl;
+ context = new Context(Context.Mode.BATCH_EDIT);
+ setEPerson(context);
+
+ if (!isAuthorized(context)) {
+ handler.logError("Current user is not eligible to execute script bulk-access-control");
+ throw new AuthorizeException("Current user is not eligible to execute script bulk-access-control");
+ }
+
+ if (uuids == null || uuids.size() == 0) {
+ handler.logError("A target uuid must be provided with at least on uuid (run with -h flag for details)");
+ throw new IllegalArgumentException("At least one target uuid must be provided");
+ }
+
+ InputStream inputStream = handler.getFileStream(context, filename)
+ .orElseThrow(() -> new IllegalArgumentException("Error reading file, the file couldn't be "
+ + "found for filename: " + filename));
+
+ try {
+ accessControl = mapper.readValue(inputStream, BulkAccessControlInput.class);
+ } catch (IOException e) {
+ handler.logError("Error parsing json file " + e.getMessage());
+ throw new IllegalArgumentException("Error parsing json file", e);
+ }
+ try {
+ validate(accessControl);
+ updateItemsAndBitstreamsPolices(accessControl);
+ context.complete();
+ } catch (Exception e) {
+ handler.handleException(e);
+ context.abort();
+ }
+ }
+
+ /**
+ * check the validation of mapped json data, it must
+ * provide item or bitstream information or both of them
+ * and check the validation of item node if provided,
+ * and check the validation of bitstream node if provided.
+ *
+ * @param accessControl mapped json data
+ * @throws SQLException if something goes wrong in the database
+ * @throws BulkAccessControlException if accessControl is invalid
+ */
+ private void validate(BulkAccessControlInput accessControl) throws SQLException {
+
+ AccessConditionItem item = accessControl.getItem();
+ AccessConditionBitstream bitstream = accessControl.getBitstream();
+
+ if (Objects.isNull(item) && Objects.isNull(bitstream)) {
+ handler.logError("item or bitstream node must be provided");
+ throw new BulkAccessControlException("item or bitstream node must be provided");
+ }
+
+ if (Objects.nonNull(item)) {
+ validateItemNode(item);
+ }
+
+ if (Objects.nonNull(bitstream)) {
+ validateBitstreamNode(bitstream);
+ }
+ }
+
+ /**
+ * check the validation of item node, the item mode
+ * must be provided with value 'add' or 'replace'
+ * if mode equals to add so the information
+ * of accessCondition must be provided,
+ * also checking that accessConditions information are valid.
+ *
+ * @param item the item node
+ * @throws BulkAccessControlException if item node is invalid
+ */
+ private void validateItemNode(AccessConditionItem item) {
+ String mode = item.getMode();
+ List accessConditions = item.getAccessConditions();
+
+ if (StringUtils.isEmpty(mode)) {
+ handler.logError("item mode node must be provided");
+ throw new BulkAccessControlException("item mode node must be provided");
+ } else if (!(StringUtils.equalsAny(mode, ADD_MODE, REPLACE_MODE))) {
+ handler.logError("wrong value for item mode<" + mode + ">");
+ throw new BulkAccessControlException("wrong value for item mode<" + mode + ">");
+ } else if (ADD_MODE.equals(mode) && isEmpty(accessConditions)) {
+ handler.logError("accessConditions of item must be provided with mode<" + ADD_MODE + ">");
+ throw new BulkAccessControlException(
+ "accessConditions of item must be provided with mode<" + ADD_MODE + ">");
+ }
+
+ for (AccessCondition accessCondition : accessConditions) {
+ validateAccessCondition(accessCondition);
+ }
+ }
+
+ /**
+ * check the validation of bitstream node, the bitstream mode
+ * must be provided with value 'add' or 'replace'
+ * if mode equals to add so the information of accessConditions
+ * must be provided,
+ * also checking that constraint information is valid,
+ * also checking that accessConditions information are valid.
+ *
+ * @param bitstream the bitstream node
+ * @throws SQLException if something goes wrong in the database
+ * @throws BulkAccessControlException if bitstream node is invalid
+ */
+ private void validateBitstreamNode(AccessConditionBitstream bitstream) throws SQLException {
+ String mode = bitstream.getMode();
+ List accessConditions = bitstream.getAccessConditions();
+
+ if (StringUtils.isEmpty(mode)) {
+ handler.logError("bitstream mode node must be provided");
+ throw new BulkAccessControlException("bitstream mode node must be provided");
+ } else if (!(StringUtils.equalsAny(mode, ADD_MODE, REPLACE_MODE))) {
+ handler.logError("wrong value for bitstream mode<" + mode + ">");
+ throw new BulkAccessControlException("wrong value for bitstream mode<" + mode + ">");
+ } else if (ADD_MODE.equals(mode) && isEmpty(accessConditions)) {
+ handler.logError("accessConditions of bitstream must be provided with mode<" + ADD_MODE + ">");
+ throw new BulkAccessControlException(
+ "accessConditions of bitstream must be provided with mode<" + ADD_MODE + ">");
+ }
+
+ validateConstraint(bitstream);
+
+ for (AccessCondition accessCondition : bitstream.getAccessConditions()) {
+ validateAccessCondition(accessCondition);
+ }
+ }
+
+ /**
+ * check the validation of constraint node if provided,
+ * constraint isn't supported when multiple uuids are provided
+ * or when uuid isn't an Item
+ *
+ * @param bitstream the bitstream node
+ * @throws SQLException if something goes wrong in the database
+ * @throws BulkAccessControlException if constraint node is invalid
+ */
+ private void validateConstraint(AccessConditionBitstream bitstream) throws SQLException {
+ if (uuids.size() > 1 && containsConstraints(bitstream)) {
+ handler.logError("constraint isn't supported when multiple uuids are provided");
+ throw new BulkAccessControlException("constraint isn't supported when multiple uuids are provided");
+ } else if (uuids.size() == 1 && containsConstraints(bitstream)) {
+ DSpaceObject dso =
+ dSpaceObjectUtils.findDSpaceObject(context, UUID.fromString(uuids.get(0)));
+
+ if (Objects.nonNull(dso) && dso.getType() != Constants.ITEM) {
+ handler.logError("constraint is not supported when uuid isn't an Item");
+ throw new BulkAccessControlException("constraint is not supported when uuid isn't an Item");
+ }
+ }
+ }
+
+ /**
+ * check the validation of access condition,
+ * the access condition name must equal to one of configured access conditions,
+ * then call {@link AccessConditionOption#validateResourcePolicy(
+ * Context, String, Date, Date)} if exception happens so, it's invalid.
+ *
+ * @param accessCondition the accessCondition
+ * @throws BulkAccessControlException if the accessCondition is invalid
+ */
+ private void validateAccessCondition(AccessCondition accessCondition) {
+
+ if (!itemAccessConditions.containsKey(accessCondition.getName())) {
+ handler.logError("wrong access condition <" + accessCondition.getName() + ">");
+ throw new BulkAccessControlException("wrong access condition <" + accessCondition.getName() + ">");
+ }
+
+ try {
+ itemAccessConditions.get(accessCondition.getName()).validateResourcePolicy(
+ context, accessCondition.getName(), accessCondition.getStartDate(), accessCondition.getEndDate());
+ } catch (Exception e) {
+ handler.logError("invalid access condition, " + e.getMessage());
+ handler.handleException(e);
+ }
+ }
+
+ /**
+ * find all items of provided {@link #uuids} from solr,
+ * then update the resource policies of items
+ * or bitstreams of items (only bitstreams of ORIGINAL bundles)
+ * and derivative bitstreams, or both of them.
+ *
+ * @param accessControl the access control input
+ * @throws SQLException if something goes wrong in the database
+ * @throws SearchServiceException if a search error occurs
+ * @throws AuthorizeException if an authorization error occurs
+ */
+ private void updateItemsAndBitstreamsPolices(BulkAccessControlInput accessControl)
+ throws SQLException, SearchServiceException, AuthorizeException {
+
+ int counter = 0;
+ int start = 0;
+ int limit = 20;
+
+ String query = buildSolrQuery(uuids);
+
+ Iterator- itemIterator = findItems(query, start, limit);
+
+ while (itemIterator.hasNext()) {
+
+ Item item = context.reloadEntity(itemIterator.next());
+
+ if (Objects.nonNull(accessControl.getItem())) {
+ updateItemPolicies(item, accessControl);
+ }
+
+ if (Objects.nonNull(accessControl.getBitstream())) {
+ updateBitstreamsPolicies(item, accessControl);
+ }
+
+ context.commit();
+ context.uncacheEntity(item);
+ counter++;
+
+ if (counter == limit) {
+ counter = 0;
+ start += limit;
+ itemIterator = findItems(query, start, limit);
+ }
+ }
+ }
+
+ private String buildSolrQuery(List uuids) throws SQLException {
+ String [] query = new String[uuids.size()];
+
+ for (int i = 0 ; i < query.length ; i++) {
+ DSpaceObject dso = dSpaceObjectUtils.findDSpaceObject(context, UUID.fromString(uuids.get(i)));
+
+ if (dso.getType() == Constants.COMMUNITY) {
+ query[i] = "location.comm:" + dso.getID();
+ } else if (dso.getType() == Constants.COLLECTION) {
+ query[i] = "location.coll:" + dso.getID();
+ } else if (dso.getType() == Constants.ITEM) {
+ query[i] = "search.resourceid:" + dso.getID();
+ }
+ }
+ return StringUtils.joinWith(" OR ", query);
+ }
+
+ private Iterator
- findItems(String query, int start, int limit)
+ throws SearchServiceException {
+
+ DiscoverQuery discoverQuery = buildDiscoveryQuery(query, start, limit);
+
+ return searchService.search(context, discoverQuery)
+ .getIndexableObjects()
+ .stream()
+ .map(indexableObject ->
+ ((IndexableItem) indexableObject).getIndexedObject())
+ .collect(Collectors.toList())
+ .iterator();
+ }
+
+ private DiscoverQuery buildDiscoveryQuery(String query, int start, int limit) {
+ DiscoverQuery discoverQuery = new DiscoverQuery();
+ discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE);
+ discoverQuery.setQuery(query);
+ discoverQuery.setStart(start);
+ discoverQuery.setMaxResults(limit);
+
+ return discoverQuery;
+ }
+
+ /**
+ * update the item resource policies,
+ * when mode equals to 'replace' will remove
+ * all current resource polices of types 'TYPE_CUSTOM'
+ * and 'TYPE_INHERITED' then, set the new resource policies.
+ *
+ * @param item the item
+ * @param accessControl the access control input
+ * @throws SQLException if something goes wrong in the database
+ * @throws AuthorizeException if an authorization error occurs
+ */
+ private void updateItemPolicies(Item item, BulkAccessControlInput accessControl)
+ throws SQLException, AuthorizeException {
+
+ AccessConditionItem acItem = accessControl.getItem();
+
+ if (REPLACE_MODE.equals(acItem.getMode())) {
+ removeReadPolicies(item, TYPE_CUSTOM);
+ removeReadPolicies(item, TYPE_INHERITED);
+ }
+
+ setItemPolicies(item, accessControl);
+ logInfo(acItem.getAccessConditions(), acItem.getMode(), item);
+ }
+
+ /**
+ * create the new resource policies of item.
+ * then, call {@link ItemService#adjustItemPolicies(
+ * Context, Item, Collection)} to adjust item's default policies.
+ *
+ * @param item the item
+ * @param accessControl the access control input
+ * @throws SQLException if something goes wrong in the database
+ * @throws AuthorizeException if an authorization error occurs
+ */
+ private void setItemPolicies(Item item, BulkAccessControlInput accessControl)
+ throws SQLException, AuthorizeException {
+
+ accessControl
+ .getItem()
+ .getAccessConditions()
+ .forEach(accessCondition -> createResourcePolicy(item, accessCondition,
+ itemAccessConditions.get(accessCondition.getName())));
+
+ itemService.adjustItemPolicies(context, item, item.getOwningCollection(), false);
+ }
+
+ /**
+ * update the resource policies of all item's bitstreams
+ * or bitstreams specified into constraint node,
+ * and derivative bitstreams.
+ *
+ * NOTE: only bitstreams of ORIGINAL bundles
+ *
+ * @param item the item contains bitstreams
+ * @param accessControl the access control input
+ */
+ private void updateBitstreamsPolicies(Item item, BulkAccessControlInput accessControl) {
+ AccessConditionBitstream.Constraint constraints = accessControl.getBitstream().getConstraints();
+
+ // look over all the bundles and force initialization of bitstreams collection
+ // to avoid lazy initialization exception
+ long count = item.getBundles()
+ .stream()
+ .flatMap(bundle ->
+ bundle.getBitstreams().stream())
+ .count();
+
+ item.getBundles(CONTENT_BUNDLE_NAME).stream()
+ .flatMap(bundle -> bundle.getBitstreams().stream())
+ .filter(bitstream -> constraints == null ||
+ constraints.getUuid() == null ||
+ constraints.getUuid().size() == 0 ||
+ constraints.getUuid().contains(bitstream.getID().toString()))
+ .forEach(bitstream -> updateBitstreamPolicies(bitstream, item, accessControl));
+ }
+
+ /**
+ * check that the bitstream node is existed,
+ * and contains constraint node,
+ * and constraint contains uuids.
+ *
+ * @param bitstream the bitstream node
+ * @return true when uuids of constraint of bitstream is not empty,
+ * otherwise false
+ */
+ private boolean containsConstraints(AccessConditionBitstream bitstream) {
+ return Objects.nonNull(bitstream) &&
+ Objects.nonNull(bitstream.getConstraints()) &&
+ isNotEmpty(bitstream.getConstraints().getUuid());
+ }
+
+ /**
+ * update the bitstream resource policies,
+ * when mode equals to replace will remove
+ * all current resource polices of types 'TYPE_CUSTOM'
+ * and 'TYPE_INHERITED' then, set the new resource policies.
+ *
+ * @param bitstream the bitstream
+ * @param item the item of bitstream
+ * @param accessControl the access control input
+ * @throws RuntimeException if something goes wrong in the database
+ * or an authorization error occurs
+ */
+ private void updateBitstreamPolicies(Bitstream bitstream, Item item, BulkAccessControlInput accessControl) {
+
+ AccessConditionBitstream acBitstream = accessControl.getBitstream();
+
+ if (REPLACE_MODE.equals(acBitstream.getMode())) {
+ removeReadPolicies(bitstream, TYPE_CUSTOM);
+ removeReadPolicies(bitstream, TYPE_INHERITED);
+ }
+
+ try {
+ setBitstreamPolicies(bitstream, item, accessControl);
+ logInfo(acBitstream.getAccessConditions(), acBitstream.getMode(), bitstream);
+ } catch (SQLException | AuthorizeException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ /**
+ * remove dspace object's read policies.
+ *
+ * @param dso the dspace object
+ * @param type resource policy type
+ * @throws BulkAccessControlException if something goes wrong
+ * in the database or an authorization error occurs
+ */
+ private void removeReadPolicies(DSpaceObject dso, String type) {
+ try {
+ resourcePolicyService.removePolicies(context, dso, type, Constants.READ);
+ } catch (SQLException | AuthorizeException e) {
+ throw new BulkAccessControlException(e);
+ }
+ }
+
+ /**
+ * create the new resource policies of bitstream.
+ * then, call {@link ItemService#adjustItemPolicies(
+ * Context, Item, Collection)} to adjust bitstream's default policies.
+ * and also update the resource policies of its derivative bitstreams.
+ *
+ * @param bitstream the bitstream
+ * @param item the item of bitstream
+ * @param accessControl the access control input
+ * @throws SQLException if something goes wrong in the database
+ * @throws AuthorizeException if an authorization error occurs
+ */
+ private void setBitstreamPolicies(Bitstream bitstream, Item item, BulkAccessControlInput accessControl)
+ throws SQLException, AuthorizeException {
+
+ accessControl.getBitstream()
+ .getAccessConditions()
+ .forEach(accessCondition -> createResourcePolicy(bitstream, accessCondition,
+ uploadAccessConditions.get(accessCondition.getName())));
+
+ itemService.adjustBitstreamPolicies(context, item, item.getOwningCollection(), bitstream);
+ mediaFilterService.updatePoliciesOfDerivativeBitstreams(context, item, bitstream);
+ }
+
+ /**
+ * create the resource policy from the information
+ * comes from the access condition.
+ *
+ * @param obj the dspace object
+ * @param accessCondition the access condition
+ * @param accessConditionOption the access condition option
+ * @throws BulkAccessControlException if an exception occurs
+ */
+ private void createResourcePolicy(DSpaceObject obj, AccessCondition accessCondition,
+ AccessConditionOption accessConditionOption) {
+
+ String name = accessCondition.getName();
+ String description = accessCondition.getDescription();
+ Date startDate = accessCondition.getStartDate();
+ Date endDate = accessCondition.getEndDate();
+
+ try {
+ accessConditionOption.createResourcePolicy(context, obj, name, description, startDate, endDate);
+ } catch (Exception e) {
+ throw new BulkAccessControlException(e);
+ }
+ }
+
+ /**
+ * Set the eperson in the context
+ *
+ * @param context the context
+ * @throws SQLException if database error
+ */
+ protected void setEPerson(Context context) throws SQLException {
+ EPerson myEPerson = epersonService.find(context, this.getEpersonIdentifier());
+
+ if (myEPerson == null) {
+ handler.logError("EPerson cannot be found: " + this.getEpersonIdentifier());
+ throw new UnsupportedOperationException("EPerson cannot be found: " + this.getEpersonIdentifier());
+ }
+
+ context.setCurrentUser(myEPerson);
+ }
+
+ private void logInfo(List accessConditions, String mode, DSpaceObject dso) {
+ String type = dso.getClass().getSimpleName();
+
+ if (REPLACE_MODE.equals(mode) && isEmpty(accessConditions)) {
+ handler.logInfo("Cleaning " + type + " {" + dso.getID() + "} policies");
+ handler.logInfo("Inheriting policies from owning Collection in " + type + " {" + dso.getID() + "}");
+ return;
+ }
+
+ StringBuilder message = new StringBuilder();
+ message.append(mode.equals(ADD_MODE) ? "Adding " : "Replacing ")
+ .append(type)
+ .append(" {")
+ .append(dso.getID())
+ .append("} policy")
+ .append(mode.equals(ADD_MODE) ? " with " : " to ")
+ .append("access conditions:");
+
+ AppendAccessConditionsInfo(message, accessConditions);
+
+ handler.logInfo(message.toString());
+
+ if (REPLACE_MODE.equals(mode) && isAppendModeEnabled()) {
+ handler.logInfo("Inheriting policies from owning Collection in " + type + " {" + dso.getID() + "}");
+ }
+ }
+
+ private void AppendAccessConditionsInfo(StringBuilder message, List accessConditions) {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ message.append("{");
+
+ for (int i = 0; i < accessConditions.size(); i++) {
+ message.append(accessConditions.get(i).getName());
+
+ Optional.ofNullable(accessConditions.get(i).getStartDate())
+ .ifPresent(date -> message.append(", start_date=" + dateFormat.format(date)));
+
+ Optional.ofNullable(accessConditions.get(i).getEndDate())
+ .ifPresent(date -> message.append(", end_date=" + dateFormat.format(date)));
+
+ if (i != accessConditions.size() - 1) {
+ message.append(", ");
+ }
+ }
+
+ message.append("}");
+ }
+
+ private boolean isAppendModeEnabled() {
+ return configurationService.getBooleanProperty("core.authorization.installitem.inheritance-read.append-mode");
+ }
+
+ protected boolean isAuthorized(Context context) {
+ return true;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public BulkAccessControlScriptConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager()
+ .getServiceByName("bulk-access-control", BulkAccessControlScriptConfiguration.class);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCli.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCli.java
new file mode 100644
index 000000000000..4e8cfe480eeb
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCli.java
@@ -0,0 +1,66 @@
+/**
+ * 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.bulkaccesscontrol;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.dspace.scripts.DSpaceCommandLineParameter;
+
+/**
+ * Extension of {@link BulkAccessControl} for CLI.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ *
+ */
+public class BulkAccessControlCli extends BulkAccessControl {
+
+ @Override
+ protected void setEPerson(Context context) throws SQLException {
+ EPerson myEPerson;
+ eperson = commandLine.getOptionValue('e');
+
+ if (eperson == null) {
+ handler.logError("An eperson to do the the Bulk Access Control must be specified " +
+ "(run with -h flag for details)");
+ throw new UnsupportedOperationException("An eperson to do the Bulk Access Control must be specified");
+ }
+
+ if (StringUtils.contains(eperson, '@')) {
+ myEPerson = epersonService.findByEmail(context, eperson);
+ } else {
+ myEPerson = epersonService.find(context, UUID.fromString(eperson));
+ }
+
+ if (myEPerson == null) {
+ handler.logError("EPerson cannot be found: " + eperson + " (run with -h flag for details)");
+ throw new UnsupportedOperationException("EPerson cannot be found: " + eperson);
+ }
+
+ context.setCurrentUser(myEPerson);
+ }
+
+ @Override
+ protected boolean isAuthorized(Context context) {
+
+ if (context.getCurrentUser() == null) {
+ return false;
+ }
+
+ return getScriptConfiguration().isAllowedToExecute(context,
+ Arrays.stream(commandLine.getOptions())
+ .map(option ->
+ new DSpaceCommandLineParameter("-" + option.getOpt(), option.getValue()))
+ .collect(Collectors.toList()));
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCliScriptConfiguration.java
new file mode 100644
index 000000000000..951c93db3030
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlCliScriptConfiguration.java
@@ -0,0 +1,42 @@
+/**
+ * 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.bulkaccesscontrol;
+
+import java.io.InputStream;
+
+import org.apache.commons.cli.Options;
+
+/**
+ * Extension of {@link BulkAccessControlScriptConfiguration} for CLI.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ *
+ */
+public class BulkAccessControlCliScriptConfiguration
+ extends BulkAccessControlScriptConfiguration {
+
+ @Override
+ public Options getOptions() {
+ Options options = new Options();
+
+ options.addOption("u", "uuid", true, "target uuids of communities/collections/items");
+ options.getOption("u").setType(String.class);
+ options.getOption("u").setRequired(true);
+
+ options.addOption("f", "file", true, "source json file");
+ options.getOption("f").setType(InputStream.class);
+ options.getOption("f").setRequired(true);
+
+ options.addOption("e", "eperson", true, "email of EPerson used to perform actions");
+ options.getOption("e").setRequired(true);
+
+ options.addOption("h", "help", false, "help");
+
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlScriptConfiguration.java
new file mode 100644
index 000000000000..5196247f94cb
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlScriptConfiguration.java
@@ -0,0 +1,110 @@
+/**
+ * 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.bulkaccesscontrol;
+
+import java.io.InputStream;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.commons.cli.Options;
+import org.dspace.app.util.DSpaceObjectUtilsImpl;
+import org.dspace.app.util.service.DSpaceObjectUtils;
+import org.dspace.content.DSpaceObject;
+import org.dspace.core.Context;
+import org.dspace.scripts.DSpaceCommandLineParameter;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+import org.dspace.utils.DSpace;
+
+/**
+ * Script configuration for {@link BulkAccessControl}.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ *
+ * @param the {@link BulkAccessControl} type
+ */
+public class BulkAccessControlScriptConfiguration extends ScriptConfiguration {
+
+ private Class dspaceRunnableClass;
+
+ @Override
+ public boolean isAllowedToExecute(Context context, List commandLineParameters) {
+
+ try {
+ if (Objects.isNull(commandLineParameters)) {
+ return authorizeService.isAdmin(context) || authorizeService.isComColAdmin(context)
+ || authorizeService.isItemAdmin(context);
+ } else {
+ List dspaceObjectIDs =
+ commandLineParameters.stream()
+ .filter(parameter -> "-u".equals(parameter.getName()))
+ .map(DSpaceCommandLineParameter::getValue)
+ .collect(Collectors.toList());
+
+ DSpaceObjectUtils dSpaceObjectUtils = new DSpace().getServiceManager().getServiceByName(
+ DSpaceObjectUtilsImpl.class.getName(), DSpaceObjectUtilsImpl.class);
+
+ for (String dspaceObjectID : dspaceObjectIDs) {
+
+ DSpaceObject dso = dSpaceObjectUtils.findDSpaceObject(context, UUID.fromString(dspaceObjectID));
+
+ if (Objects.isNull(dso)) {
+ throw new IllegalArgumentException();
+ }
+
+ if (!authorizeService.isAdmin(context, dso)) {
+ return false;
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ return true;
+ }
+
+ @Override
+ public Options getOptions() {
+ if (options == null) {
+ Options options = new Options();
+
+ options.addOption("u", "uuid", true, "target uuids of communities/collections/items");
+ options.getOption("u").setType(String.class);
+ options.getOption("u").setRequired(true);
+
+ options.addOption("f", "file", true, "source json file");
+ options.getOption("f").setType(InputStream.class);
+ options.getOption("f").setRequired(true);
+
+ options.addOption("h", "help", false, "help");
+
+ super.options = options;
+ }
+ return options;
+ }
+
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableClass;
+ }
+
+ /**
+ * Generic setter for the dspaceRunnableClass
+ *
+ * @param dspaceRunnableClass The dspaceRunnableClass to be set on this
+ * BulkImportScriptConfiguration
+ */
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableClass = dspaceRunnableClass;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/exception/BulkAccessControlException.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/exception/BulkAccessControlException.java
new file mode 100644
index 000000000000..092611eb0654
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/exception/BulkAccessControlException.java
@@ -0,0 +1,48 @@
+/**
+ * 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.bulkaccesscontrol.exception;
+
+/**
+ * Exception for errors that occurs during the bulk access control
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ *
+ */
+public class BulkAccessControlException extends RuntimeException {
+
+ private static final long serialVersionUID = -74730626862418515L;
+
+ /**
+ * Constructor with error message and cause.
+ *
+ * @param message the error message
+ * @param cause the error cause
+ */
+ public BulkAccessControlException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructor with error message.
+ *
+ * @param message the error message
+ */
+ public BulkAccessControlException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor with error cause.
+ *
+ * @param cause the error cause
+ */
+ public BulkAccessControlException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessCondition.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessCondition.java
new file mode 100644
index 000000000000..6cf95e0e2179
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessCondition.java
@@ -0,0 +1,59 @@
+/**
+ * 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.bulkaccesscontrol.model;
+
+import java.util.Date;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.dspace.app.bulkaccesscontrol.BulkAccessControl;
+import org.dspace.util.MultiFormatDateDeserializer;
+
+/**
+ * Class that model the values of an Access Condition as expressed in the {@link BulkAccessControl} input file
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class AccessCondition {
+
+ private String name;
+
+ private String description;
+
+ @JsonDeserialize(using = MultiFormatDateDeserializer.class)
+ private Date startDate;
+
+ @JsonDeserialize(using = MultiFormatDateDeserializer.class)
+ private Date endDate;
+
+ public AccessCondition() {
+ }
+
+ public AccessCondition(String name, String description, Date startDate, Date endDate) {
+ this.name = name;
+ this.description = description;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionBitstream.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionBitstream.java
new file mode 100644
index 000000000000..2176e24d7f9d
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionBitstream.java
@@ -0,0 +1,69 @@
+/**
+ * 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.bulkaccesscontrol.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dspace.app.bulkaccesscontrol.BulkAccessControl;
+
+/**
+ * Class that model the value of bitstream node
+ * from json file of the {@link BulkAccessControl}
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class AccessConditionBitstream {
+
+ private String mode;
+
+ private Constraint constraints;
+
+ private List accessConditions;
+
+ public String getMode() {
+ return mode;
+ }
+
+ public void setMode(String mode) {
+ this.mode = mode;
+ }
+
+ public Constraint getConstraints() {
+ return constraints;
+ }
+
+ public void setConstraints(Constraint constraints) {
+ this.constraints = constraints;
+ }
+
+ public List getAccessConditions() {
+ if (accessConditions == null) {
+ return new ArrayList<>();
+ }
+ return accessConditions;
+ }
+
+ public void setAccessConditions(List accessConditions) {
+ this.accessConditions = accessConditions;
+ }
+
+ public class Constraint {
+
+ private List uuid;
+
+ public List getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(List uuid) {
+ this.uuid = uuid;
+ }
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionItem.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionItem.java
new file mode 100644
index 000000000000..c482dfc34d65
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/AccessConditionItem.java
@@ -0,0 +1,45 @@
+/**
+ * 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.bulkaccesscontrol.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dspace.app.bulkaccesscontrol.BulkAccessControl;
+
+/**
+ * Class that model the value of item node
+ * from json file of the {@link BulkAccessControl}
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class AccessConditionItem {
+
+ String mode;
+
+ List accessConditions;
+
+ public String getMode() {
+ return mode;
+ }
+
+ public void setMode(String mode) {
+ this.mode = mode;
+ }
+
+ public List getAccessConditions() {
+ if (accessConditions == null) {
+ return new ArrayList<>();
+ }
+ return accessConditions;
+ }
+
+ public void setAccessConditions(List accessConditions) {
+ this.accessConditions = accessConditions;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessConditionConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessConditionConfiguration.java
new file mode 100644
index 000000000000..a2ebbe5a12d4
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessConditionConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * 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.bulkaccesscontrol.model;
+
+import java.util.List;
+
+import org.dspace.submit.model.AccessConditionOption;
+
+/**
+ * A collection of conditions to be met when bulk access condition.
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class BulkAccessConditionConfiguration {
+
+ private String name;
+ private List itemAccessConditionOptions;
+ private List bitstreamAccessConditionOptions;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getItemAccessConditionOptions() {
+ return itemAccessConditionOptions;
+ }
+
+ public void setItemAccessConditionOptions(
+ List itemAccessConditionOptions) {
+ this.itemAccessConditionOptions = itemAccessConditionOptions;
+ }
+
+ public List getBitstreamAccessConditionOptions() {
+ return bitstreamAccessConditionOptions;
+ }
+
+ public void setBitstreamAccessConditionOptions(
+ List bitstreamAccessConditionOptions) {
+ this.bitstreamAccessConditionOptions = bitstreamAccessConditionOptions;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessControlInput.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessControlInput.java
new file mode 100644
index 000000000000..0f8852a71f7d
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/model/BulkAccessControlInput.java
@@ -0,0 +1,72 @@
+/**
+ * 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.bulkaccesscontrol.model;
+
+import org.dspace.app.bulkaccesscontrol.BulkAccessControl;
+
+/**
+ * Class that model the content of the JSON file used as input for the {@link BulkAccessControl}
+ *
+ *
+ * {
+ * item: {
+ * mode: "replace",
+ * accessConditions: [
+ * {
+ * "name": "openaccess"
+ * }
+ * ]
+ * },
+ * bitstream: {
+ * constraints: {
+ * uuid: [bit-uuid1, bit-uuid2, ..., bit-uuidN],
+ * },
+ * mode: "add",
+ * accessConditions: [
+ * {
+ * "name": "embargo",
+ * "startDate": "2024-06-24T23:59:59.999+0000"
+ * }
+ * ]
+ * }
+ * }
+ *
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class BulkAccessControlInput {
+
+ AccessConditionItem item;
+
+ AccessConditionBitstream bitstream;
+
+ public BulkAccessControlInput() {
+ }
+
+ public BulkAccessControlInput(AccessConditionItem item,
+ AccessConditionBitstream bitstream) {
+ this.item = item;
+ this.bitstream = bitstream;
+ }
+
+ public AccessConditionItem getItem() {
+ return item;
+ }
+
+ public void setItem(AccessConditionItem item) {
+ this.item = item;
+ }
+
+ public AccessConditionBitstream getBitstream() {
+ return bitstream;
+ }
+
+ public void setBitstream(AccessConditionBitstream bitstream) {
+ this.bitstream = bitstream;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/service/BulkAccessConditionConfigurationService.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/service/BulkAccessConditionConfigurationService.java
new file mode 100644
index 000000000000..321b6d928e92
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/service/BulkAccessConditionConfigurationService.java
@@ -0,0 +1,45 @@
+/**
+ * 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.bulkaccesscontrol.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.dspace.app.bulkaccesscontrol.model.BulkAccessConditionConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Simple bean to manage different Bulk Access Condition configurations
+ *
+ * @author Mohamed Eskander (mohamed.eskander at 4science.it)
+ */
+public class BulkAccessConditionConfigurationService {
+
+ @Autowired
+ private List bulkAccessConditionConfigurations;
+
+ public List getBulkAccessConditionConfigurations() {
+ if (CollectionUtils.isEmpty(bulkAccessConditionConfigurations)) {
+ return new ArrayList<>();
+ }
+ return bulkAccessConditionConfigurations;
+ }
+
+ public BulkAccessConditionConfiguration getBulkAccessConditionConfiguration(String name) {
+ return getBulkAccessConditionConfigurations().stream()
+ .filter(x -> name.equals(x.getName()))
+ .findFirst()
+ .orElse(null);
+ }
+
+ public void setBulkAccessConditionConfigurations(
+ List bulkAccessConditionConfigurations) {
+ this.bulkAccessConditionConfigurations = bulkAccessConditionConfigurations;
+ }
+}
\ No newline at end of file
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java
index b8d41318db48..fb228e7041b8 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java
@@ -7,33 +7,16 @@
*/
package org.dspace.app.bulkedit;
-import java.sql.SQLException;
-
import org.apache.commons.cli.Options;
-import org.dspace.authorize.service.AuthorizeService;
-import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
-import org.springframework.beans.factory.annotation.Autowired;
/**
* The {@link ScriptConfiguration} for the {@link MetadataDeletion} script.
*/
public class MetadataDeletionScriptConfiguration extends ScriptConfiguration {
- @Autowired
- private AuthorizeService authorizeService;
-
private Class dspaceRunnableClass;
- @Override
- public boolean isAllowedToExecute(Context context) {
- try {
- return authorizeService.isAdmin(context);
- } catch (SQLException e) {
- throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
- }
- }
-
@Override
public Options getOptions() {
if (options == null) {
@@ -41,10 +24,8 @@ public Options getOptions() {
Options options = new Options();
options.addOption("m", "metadata", true, "metadata field name");
- options.getOption("m").setType(String.class);
options.addOption("l", "list", false, "lists the metadata fields that can be deleted");
- options.getOption("l").setType(boolean.class);
super.options = options;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java
index 0c513c466722..aa76c09c0a5b 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java
@@ -7,22 +7,14 @@
*/
package org.dspace.app.bulkedit;
-import java.sql.SQLException;
-
import org.apache.commons.cli.Options;
-import org.dspace.authorize.service.AuthorizeService;
-import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
-import org.springframework.beans.factory.annotation.Autowired;
/**
* The {@link ScriptConfiguration} for the {@link MetadataExport} script
*/
public class MetadataExportScriptConfiguration extends ScriptConfiguration {
- @Autowired
- private AuthorizeService authorizeService;
-
private Class dspaceRunnableClass;
@Override
@@ -39,27 +31,15 @@ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
- @Override
- public boolean isAllowedToExecute(Context context) {
- try {
- return authorizeService.isAdmin(context);
- } catch (SQLException e) {
- throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
- }
- }
-
@Override
public Options getOptions() {
if (options == null) {
Options options = new Options();
options.addOption("i", "id", true, "ID or handle of thing to export (item, collection, or community)");
- options.getOption("i").setType(String.class);
options.addOption("a", "all", false,
"include all metadata fields that are not normally changed (e.g. provenance)");
- options.getOption("a").setType(boolean.class);
options.addOption("h", "help", false, "help");
- options.getOption("h").setType(boolean.class);
super.options = options;
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java
new file mode 100644
index 000000000000..027ad116a7e2
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java
@@ -0,0 +1,170 @@
+/**
+ * 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.bulkedit;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.commons.cli.ParseException;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataDSpaceCsvExportServiceImpl;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.CollectionService;
+import org.dspace.content.service.CommunityService;
+import org.dspace.content.service.MetadataDSpaceCsvExportService;
+import org.dspace.core.Context;
+import org.dspace.discovery.DiscoverQuery;
+import org.dspace.discovery.IndexableObject;
+import org.dspace.discovery.SearchService;
+import org.dspace.discovery.SearchUtils;
+import org.dspace.discovery.configuration.DiscoveryConfiguration;
+import org.dspace.discovery.configuration.DiscoveryConfigurationService;
+import org.dspace.discovery.indexobject.IndexableCollection;
+import org.dspace.discovery.indexobject.IndexableCommunity;
+import org.dspace.discovery.utils.DiscoverQueryBuilder;
+import org.dspace.discovery.utils.parameter.QueryBuilderSearchFilter;
+import org.dspace.eperson.factory.EPersonServiceFactory;
+import org.dspace.eperson.service.EPersonService;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.sort.SortOption;
+import org.dspace.utils.DSpace;
+
+/**
+ * Metadata exporter to allow the batch export of metadata from a discovery search into a file
+ *
+ */
+public class MetadataExportSearch extends DSpaceRunnable {
+ private static final String EXPORT_CSV = "exportCSV";
+ private boolean help = false;
+ private String identifier;
+ private String discoveryConfigName;
+ private String[] filterQueryStrings;
+ private boolean hasScope = false;
+ private String query;
+
+ private SearchService searchService;
+ private MetadataDSpaceCsvExportService metadataDSpaceCsvExportService;
+ private EPersonService ePersonService;
+ private DiscoveryConfigurationService discoveryConfigurationService;
+ private CommunityService communityService;
+ private CollectionService collectionService;
+ private DiscoverQueryBuilder queryBuilder;
+
+ @Override
+ public MetadataExportSearchScriptConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager()
+ .getServiceByName("metadata-export-search", MetadataExportSearchScriptConfiguration.class);
+ }
+
+ @Override
+ public void setup() throws ParseException {
+ searchService = SearchUtils.getSearchService();
+ metadataDSpaceCsvExportService = new DSpace().getServiceManager()
+ .getServiceByName(
+ MetadataDSpaceCsvExportServiceImpl.class.getCanonicalName(),
+ MetadataDSpaceCsvExportService.class
+ );
+ ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
+ discoveryConfigurationService = SearchUtils.getConfigurationService();
+ communityService = ContentServiceFactory.getInstance().getCommunityService();
+ collectionService = ContentServiceFactory.getInstance().getCollectionService();
+ queryBuilder = SearchUtils.getQueryBuilder();
+
+ if (commandLine.hasOption('h')) {
+ help = true;
+ return;
+ }
+
+ if (commandLine.hasOption('q')) {
+ query = commandLine.getOptionValue('q');
+ }
+
+ if (commandLine.hasOption('s')) {
+ hasScope = true;
+ identifier = commandLine.getOptionValue('s');
+ }
+
+ if (commandLine.hasOption('c')) {
+ discoveryConfigName = commandLine.getOptionValue('c');
+ }
+
+ if (commandLine.hasOption('f')) {
+ filterQueryStrings = commandLine.getOptionValues('f');
+ }
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+ if (help) {
+ loghelpinfo();
+ printHelp();
+ return;
+ }
+ handler.logDebug("starting search export");
+
+ IndexableObject dso = null;
+ Context context = new Context();
+ context.setCurrentUser(ePersonService.find(context, this.getEpersonIdentifier()));
+
+ if (hasScope) {
+ dso = resolveScope(context, identifier);
+ }
+
+ DiscoveryConfiguration discoveryConfiguration =
+ discoveryConfigurationService.getDiscoveryConfiguration(discoveryConfigName);
+
+ List queryBuilderSearchFilters = new ArrayList<>();
+
+ handler.logDebug("processing filter queries");
+ if (filterQueryStrings != null) {
+ for (String filterQueryString: filterQueryStrings) {
+ String field = filterQueryString.split(",", 2)[0];
+ String operator = filterQueryString.split("(,|=)", 3)[1];
+ String value = filterQueryString.split("=", 2)[1];
+ QueryBuilderSearchFilter queryBuilderSearchFilter =
+ new QueryBuilderSearchFilter(field, operator, value);
+ queryBuilderSearchFilters.add(queryBuilderSearchFilter);
+ }
+ }
+ handler.logDebug("building query");
+ DiscoverQuery discoverQuery =
+ queryBuilder.buildQuery(context, dso, discoveryConfiguration, query, queryBuilderSearchFilters,
+ "Item", 10, Long.getLong("0"), null, SortOption.DESCENDING);
+ handler.logDebug("creating iterator");
+
+ Iterator- itemIterator = searchService.iteratorSearch(context, dso, discoverQuery);
+ handler.logDebug("creating dspacecsv");
+ DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService.export(context, itemIterator, true);
+ handler.logDebug("writing to file " + getFileNameOrExportFile());
+ handler.writeFilestream(context, getFileNameOrExportFile(), dSpaceCSV.getInputStream(), EXPORT_CSV);
+ context.restoreAuthSystemState();
+ context.complete();
+
+ }
+
+ protected void loghelpinfo() {
+ handler.logInfo("metadata-export");
+ }
+
+ protected String getFileNameOrExportFile() {
+ return "metadataExportSearch.csv";
+ }
+
+ public IndexableObject resolveScope(Context context, String id) throws SQLException {
+ UUID uuid = UUID.fromString(id);
+ IndexableObject scopeObj = new IndexableCommunity(communityService.find(context, uuid));
+ if (scopeObj.getIndexedObject() == null) {
+ scopeObj = new IndexableCollection(collectionService.find(context, uuid));
+ }
+ return scopeObj;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCli.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCli.java
new file mode 100644
index 000000000000..51ca77cbfb3a
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCli.java
@@ -0,0 +1,20 @@
+/**
+ * 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.bulkedit;
+
+/**
+ * The cli version of the {@link MetadataExportSearch} script
+ */
+public class MetadataExportSearchCli extends MetadataExportSearch {
+
+ @Override
+ protected String getFileNameOrExportFile() {
+ return commandLine.getOptionValue('n');
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCliScriptConfiguration.java
new file mode 100644
index 000000000000..c0343f545a98
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchCliScriptConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * 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.bulkedit;
+
+import org.apache.commons.cli.Options;
+
+/**
+ * This is the CLI version of the {@link MetadataExportSearchScriptConfiguration} class that handles the
+ * configuration for the {@link MetadataExportSearchCli} script
+ */
+public class MetadataExportSearchCliScriptConfiguration
+ extends MetadataExportSearchScriptConfiguration {
+
+ @Override
+ public Options getOptions() {
+ Options options = super.getOptions();
+ options.addOption("n", "filename", true, "the filename to export to");
+ return super.getOptions();
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java
new file mode 100644
index 000000000000..4f2a225d3ac6
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java
@@ -0,0 +1,56 @@
+/**
+ * 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.bulkedit;
+
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link MetadataExportSearch} script
+ */
+public class MetadataExportSearchScriptConfiguration extends ScriptConfiguration {
+
+ private Class dspaceRunnableclass;
+
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableclass;
+ }
+
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableclass = dspaceRunnableClass;
+ }
+
+ @Override
+ public Options getOptions() {
+ if (options == null) {
+ Options options = new Options();
+ options.addOption("q", "query", true,
+ "The discovery search string to will be used to match records. Not URL encoded");
+ options.getOption("q").setType(String.class);
+ options.addOption("s", "scope", true,
+ "UUID of a specific DSpace container (site, community or collection) to which the search has to be " +
+ "limited");
+ options.getOption("s").setType(String.class);
+ options.addOption("c", "configuration", true,
+ "The name of a Discovery configuration that should be used by this search");
+ options.getOption("c").setType(String.class);
+ options.addOption("f", "filter", true,
+ "Advanced search filter that has to be used to filter the result set, with syntax `<:filter-name>," +
+ "<:filter-operator>=<:filter-value>`. Not URL encoded. For example `author," +
+ "authority=5df05073-3be7-410d-8166-e254369e4166` or `title,contains=sample text`");
+ options.getOption("f").setType(String.class);
+ options.addOption("h", "help", false, "help");
+
+ super.options = options;
+ }
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
index 469245908a84..af6976acb14a 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java
@@ -578,6 +578,10 @@ public List runImport(Context c, boolean change,
wfItem = workflowService.startWithoutNotify(c, wsItem);
}
} else {
+ // Add provenance info
+ String provenance = installItemService.getSubmittedByProvenanceMessage(c, wsItem.getItem());
+ itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(),
+ "description", "provenance", "en", provenance);
// Install the item
installItemService.installItem(c, wsItem);
}
@@ -598,18 +602,19 @@ public List runImport(Context c, boolean change,
changes.add(whatHasChanged);
}
- if (change) {
- //only clear cache if changes have been made.
- c.uncacheEntity(wsItem);
- c.uncacheEntity(wfItem);
- c.uncacheEntity(item);
+ if (change && (rowCount % configurationService.getIntProperty("bulkedit.change.commit.count", 100) == 0)) {
+ c.commit();
+ handler.logInfo(LogHelper.getHeader(c, "metadata_import_commit", "lineNumber=" + rowCount));
}
populateRefAndRowMap(line, item == null ? null : item.getID());
// keep track of current rows processed
rowCount++;
}
+ if (change) {
+ c.commit();
+ }
- c.setMode(originalMode);
+ c.setMode(Context.Mode.READ_ONLY);
// Return the changes
@@ -925,11 +930,10 @@ private void addRelationship(Context c, Item item, String typeName, String value
rightItem = item;
}
- // Create the relationship
- int leftPlace = relationshipService.findNextLeftPlaceByLeftItem(c, leftItem);
- int rightPlace = relationshipService.findNextRightPlaceByRightItem(c, rightItem);
- Relationship persistedRelationship = relationshipService.create(c, leftItem, rightItem,
- foundRelationshipType, leftPlace, rightPlace);
+ // Create the relationship, appending to the end
+ Relationship persistedRelationship = relationshipService.create(
+ c, leftItem, rightItem, foundRelationshipType, -1, -1
+ );
relationshipService.update(c, persistedRelationship);
}
@@ -1363,7 +1367,7 @@ private int displayChanges(List changes, boolean changed) {
* is the field is defined as authority controlled
*/
private static boolean isAuthorityControlledField(String md) {
- String mdf = StringUtils.substringAfter(md, ":");
+ String mdf = md.contains(":") ? StringUtils.substringAfter(md, ":") : md;
mdf = StringUtils.substringBefore(mdf, "[");
return authorityControlled.contains(mdf);
}
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportCliScriptConfiguration.java
index 038df616cae5..7e1537fe9d91 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportCliScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportCliScriptConfiguration.java
@@ -19,7 +19,6 @@ public class MetadataImportCliScriptConfiguration extends MetadataImportScriptCo
public Options getOptions() {
Options options = super.getOptions();
options.addOption("e", "email", true, "email address or user id of user (required if adding new items)");
- options.getOption("e").setType(String.class);
options.getOption("e").setRequired(true);
super.options = options;
return options;
diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportScriptConfiguration.java
index 07e6a9aec96e..ce2f7fb68af1 100644
--- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImportScriptConfiguration.java
@@ -8,22 +8,15 @@
package org.dspace.app.bulkedit;
import java.io.InputStream;
-import java.sql.SQLException;
import org.apache.commons.cli.Options;
-import org.dspace.authorize.service.AuthorizeService;
-import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
-import org.springframework.beans.factory.annotation.Autowired;
/**
* The {@link ScriptConfiguration} for the {@link MetadataImport} script
*/
public class MetadataImportScriptConfiguration extends ScriptConfiguration {
- @Autowired
- private AuthorizeService authorizeService;
-
private Class dspaceRunnableClass;
@Override
@@ -40,15 +33,6 @@ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
- @Override
- public boolean isAllowedToExecute(Context context) {
- try {
- return authorizeService.isAdmin(context);
- } catch (SQLException e) {
- throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
- }
- }
-
@Override
public Options getOptions() {
if (options == null) {
@@ -59,20 +43,14 @@ public Options getOptions() {
options.getOption("f").setRequired(true);
options.addOption("s", "silent", false,
"silent operation - doesn't request confirmation of changes USE WITH CAUTION");
- options.getOption("s").setType(boolean.class);
options.addOption("w", "workflow", false, "workflow - when adding new items, use collection workflow");
- options.getOption("w").setType(boolean.class);
options.addOption("n", "notify", false,
"notify - when adding new items using a workflow, send notification emails");
- options.getOption("n").setType(boolean.class);
options.addOption("v", "validate-only", false,
"validate - just validate the csv, don't run the import");
- options.getOption("v").setType(boolean.class);
options.addOption("t", "template", false,
"template - when adding new items, use the collection template (if it exists)");
- options.getOption("t").setType(boolean.class);
options.addOption("h", "help", false, "help");
- options.getOption("h").setType(boolean.class);
super.options = options;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java b/dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java
new file mode 100644
index 000000000000..8291af87fc2e
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/exception/ResourceAlreadyExistsException.java
@@ -0,0 +1,32 @@
+/**
+ * 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.exception;
+
+/**
+ * This class provides an exception to be used when trying to save a resource
+ * that already exists.
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class ResourceAlreadyExistsException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a ResourceAlreadyExistsException with a message and the already
+ * existing resource.
+ *
+ * @param message the error message
+ */
+ public ResourceAlreadyExistsException(String message) {
+ super(message);
+ }
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java
index 629056214346..ff83c3ecb225 100644
--- a/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java
@@ -7,18 +7,11 @@
*/
package org.dspace.app.harvest;
-import java.sql.SQLException;
-
import org.apache.commons.cli.Options;
-import org.dspace.authorize.service.AuthorizeService;
-import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
-import org.springframework.beans.factory.annotation.Autowired;
public class HarvestScriptConfiguration extends ScriptConfiguration {
- @Autowired
- private AuthorizeService authorizeService;
private Class dspaceRunnableClass;
@@ -32,33 +25,18 @@ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
- public boolean isAllowedToExecute(final Context context) {
- try {
- return authorizeService.isAdmin(context);
- } catch (SQLException e) {
- throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
- }
- }
public Options getOptions() {
Options options = new Options();
options.addOption("p", "purge", false, "delete all items in the collection");
- options.getOption("p").setType(boolean.class);
options.addOption("r", "run", false, "run the standard harvest procedure");
- options.getOption("r").setType(boolean.class);
options.addOption("g", "ping", false, "test the OAI server and set");
- options.getOption("g").setType(boolean.class);
options.addOption("s", "setup", false, "Set the collection up for harvesting");
- options.getOption("s").setType(boolean.class);
options.addOption("S", "start", false, "start the harvest loop");
- options.getOption("S").setType(boolean.class);
options.addOption("R", "reset", false, "reset harvest status on all collections");
- options.getOption("R").setType(boolean.class);
options.addOption("P", "purgeCollections", false, "purge all harvestable collections");
- options.getOption("P").setType(boolean.class);
options.addOption("o", "reimport", false, "reimport all items in the collection, " +
"this is equivalent to -p -r, purging all items in a collection and reimporting them");
- options.getOption("o").setType(boolean.class);
options.addOption("c", "collection", true,
"harvesting collection (handle or id)");
options.addOption("t", "type", true,
@@ -72,7 +50,6 @@ public Options getOptions() {
"crosswalk in dspace.cfg");
options.addOption("h", "help", false, "help");
- options.getOption("h").setType(boolean.class);
return options;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java
new file mode 100644
index 000000000000..71fc088694d9
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExport.java
@@ -0,0 +1,264 @@
+/**
+ * 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.itemexport;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.io.file.PathUtils;
+import org.dspace.app.itemexport.factory.ItemExportServiceFactory;
+import org.dspace.app.itemexport.service.ItemExportService;
+import org.dspace.content.Collection;
+import org.dspace.content.Item;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.CollectionService;
+import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.dspace.eperson.factory.EPersonServiceFactory;
+import org.dspace.eperson.service.EPersonService;
+import org.dspace.handle.factory.HandleServiceFactory;
+import org.dspace.handle.service.HandleService;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.utils.DSpace;
+
+/**
+ * Item exporter to create simple AIPs for DSpace content. Currently exports
+ * individual items, or entire collections. For instructions on use, see
+ * printUsage() method.
+ *
+ * ItemExport creates the simple AIP package that the importer also uses. It
+ * consists of:
+ *
+ * /exportdir/42/ (one directory per item) / dublin_core.xml - qualified dublin
+ * core in RDF schema / contents - text file, listing one file per line / file1
+ * - files contained in the item / file2 / ...
+ *
+ * issues -doesn't handle special characters in metadata (needs to turn {@code &'s} into
+ * {@code &}, etc.)
+ *
+ * Modified by David Little, UCSD Libraries 12/21/04 to allow the registration
+ * of files (bitstreams) into DSpace.
+ *
+ * @author David Little
+ * @author Jay Paz
+ */
+public class ItemExport extends DSpaceRunnable {
+
+ public static final String TEMP_DIR = "exportSAF";
+ public static final String ZIP_NAME = "exportSAFZip";
+ public static final String ZIP_FILENAME = "saf-export";
+ public static final String ZIP_EXT = "zip";
+
+ protected String typeString = null;
+ protected String destDirName = null;
+ protected String idString = null;
+ protected int seqStart = -1;
+ protected int type = -1;
+ protected Item item = null;
+ protected Collection collection = null;
+ protected boolean migrate = false;
+ protected boolean zip = false;
+ protected String zipFileName = "";
+ protected boolean excludeBitstreams = false;
+ protected boolean help = false;
+
+ protected static HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
+ protected static ItemService itemService = ContentServiceFactory.getInstance().getItemService();
+ protected static CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
+ protected static final EPersonService epersonService =
+ EPersonServiceFactory.getInstance().getEPersonService();
+
+ @Override
+ public ItemExportScriptConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager()
+ .getServiceByName("export", ItemExportScriptConfiguration.class);
+ }
+
+ @Override
+ public void setup() throws ParseException {
+ help = commandLine.hasOption('h');
+
+ if (commandLine.hasOption('t')) { // type
+ typeString = commandLine.getOptionValue('t');
+
+ if ("ITEM".equals(typeString)) {
+ type = Constants.ITEM;
+ } else if ("COLLECTION".equals(typeString)) {
+ type = Constants.COLLECTION;
+ }
+ }
+
+ if (commandLine.hasOption('i')) { // id
+ idString = commandLine.getOptionValue('i');
+ }
+
+ setNumber();
+
+ if (commandLine.hasOption('m')) { // number
+ migrate = true;
+ }
+
+ if (commandLine.hasOption('x')) {
+ excludeBitstreams = true;
+ }
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+ if (help) {
+ printHelp();
+ return;
+ }
+
+ validate();
+
+ Context context = new Context();
+ context.turnOffAuthorisationSystem();
+
+ if (type == Constants.ITEM) {
+ // first, is myIDString a handle?
+ if (idString.indexOf('/') != -1) {
+ item = (Item) handleService.resolveToObject(context, idString);
+
+ if ((item == null) || (item.getType() != Constants.ITEM)) {
+ item = null;
+ }
+ } else {
+ item = itemService.find(context, UUID.fromString(idString));
+ }
+
+ if (item == null) {
+ handler.logError("The item cannot be found: " + idString + " (run with -h flag for details)");
+ throw new UnsupportedOperationException("The item cannot be found: " + idString);
+ }
+ } else {
+ if (idString.indexOf('/') != -1) {
+ // has a / must be a handle
+ collection = (Collection) handleService.resolveToObject(context,
+ idString);
+
+ // ensure it's a collection
+ if ((collection == null)
+ || (collection.getType() != Constants.COLLECTION)) {
+ collection = null;
+ }
+ } else {
+ collection = collectionService.find(context, UUID.fromString(idString));
+ }
+
+ if (collection == null) {
+ handler.logError("The collection cannot be found: " + idString + " (run with -h flag for details)");
+ throw new UnsupportedOperationException("The collection cannot be found: " + idString);
+ }
+ }
+
+ ItemExportService itemExportService = ItemExportServiceFactory.getInstance()
+ .getItemExportService();
+ try {
+ itemExportService.setHandler(handler);
+ process(context, itemExportService);
+ context.complete();
+ } catch (Exception e) {
+ context.abort();
+ throw new Exception(e);
+ }
+ }
+
+ /**
+ * Validate the options
+ */
+ protected void validate() {
+ if (type == -1) {
+ handler.logError("The type must be either COLLECTION or ITEM (run with -h flag for details)");
+ throw new UnsupportedOperationException("The type must be either COLLECTION or ITEM");
+ }
+
+ if (idString == null) {
+ handler.logError("The ID must be set to either a database ID or a handle (run with -h flag for details)");
+ throw new UnsupportedOperationException("The ID must be set to either a database ID or a handle");
+ }
+ }
+
+ /**
+ * Process the export
+ * @param context
+ * @throws Exception
+ */
+ protected void process(Context context, ItemExportService itemExportService) throws Exception {
+ setEPerson(context);
+ setDestDirName(context, itemExportService);
+ setZip(context);
+
+ Iterator- items;
+ if (item != null) {
+ List
- myItems = new ArrayList<>();
+ myItems.add(item);
+ items = myItems.iterator();
+ } else {
+ handler.logInfo("Exporting from collection: " + idString);
+ items = itemService.findByCollection(context, collection);
+ }
+ itemExportService.exportAsZip(context, items, destDirName, zipFileName,
+ seqStart, migrate, excludeBitstreams);
+
+ File zip = new File(destDirName + System.getProperty("file.separator") + zipFileName);
+ try (InputStream is = new FileInputStream(zip)) {
+ // write input stream on handler
+ handler.writeFilestream(context, ZIP_FILENAME + "." + ZIP_EXT, is, ZIP_NAME);
+ } finally {
+ PathUtils.deleteDirectory(Path.of(destDirName));
+ }
+ }
+
+ /**
+ * Set the destination directory option
+ */
+ protected void setDestDirName(Context context, ItemExportService itemExportService) throws Exception {
+ destDirName = itemExportService.getExportWorkDirectory() + File.separator + TEMP_DIR;
+ }
+
+ /**
+ * Set the zip option
+ */
+ protected void setZip(Context context) {
+ zip = true;
+ zipFileName = ZIP_FILENAME + "-" + context.getCurrentUser().getID() + "." + ZIP_EXT;
+ }
+
+ /**
+ * Set the number option
+ */
+ protected void setNumber() {
+ seqStart = 1;
+ if (commandLine.hasOption('n')) { // number
+ seqStart = Integer.parseInt(commandLine.getOptionValue('n'));
+ }
+ }
+
+ private void setEPerson(Context context) throws SQLException {
+ EPerson myEPerson = epersonService.find(context, this.getEpersonIdentifier());
+
+ // check eperson
+ if (myEPerson == null) {
+ handler.logError("EPerson cannot be found: " + this.getEpersonIdentifier());
+ throw new UnsupportedOperationException("EPerson cannot be found: " + this.getEpersonIdentifier());
+ }
+
+ context.setCurrentUser(myEPerson);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLI.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLI.java
new file mode 100644
index 000000000000..8e9af1e01094
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLI.java
@@ -0,0 +1,96 @@
+/**
+ * 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.itemexport;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.dspace.app.itemexport.service.ItemExportService;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+
+/**
+ * CLI variant for the {@link ItemExport} class.
+ * This was done to specify the specific behaviors for the CLI.
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemExportCLI extends ItemExport {
+
+ @Override
+ protected void validate() {
+ super.validate();
+
+ setDestDirName();
+
+ if (destDirName == null) {
+ handler.logError("The destination directory must be set (run with -h flag for details)");
+ throw new UnsupportedOperationException("The destination directory must be set");
+ }
+
+ if (seqStart == -1) {
+ handler.logError("The sequence start number must be set (run with -h flag for details)");
+ throw new UnsupportedOperationException("The sequence start number must be set");
+ }
+ }
+
+ @Override
+ protected void process(Context context, ItemExportService itemExportService) throws Exception {
+ setZip(context);
+
+ if (zip) {
+ Iterator
- items;
+ if (item != null) {
+ List
- myItems = new ArrayList<>();
+ myItems.add(item);
+ items = myItems.iterator();
+ } else {
+ handler.logInfo("Exporting from collection: " + idString);
+ items = itemService.findByCollection(context, collection);
+ }
+ itemExportService.exportAsZip(context, items, destDirName, zipFileName,
+ seqStart, migrate, excludeBitstreams);
+ } else {
+ if (item != null) {
+ // it's only a single item
+ itemExportService
+ .exportItem(context, Collections.singletonList(item).iterator(), destDirName,
+ seqStart, migrate, excludeBitstreams);
+ } else {
+ handler.logInfo("Exporting from collection: " + idString);
+
+ // it's a collection, so do a bunch of items
+ Iterator
- i = itemService.findByCollection(context, collection);
+ itemExportService.exportItem(context, i, destDirName, seqStart, migrate, excludeBitstreams);
+ }
+ }
+ }
+
+ protected void setDestDirName() {
+ if (commandLine.hasOption('d')) { // dest
+ destDirName = commandLine.getOptionValue('d');
+ }
+ }
+
+ @Override
+ protected void setZip(Context context) {
+ if (commandLine.hasOption('z')) {
+ zip = true;
+ zipFileName = commandLine.getOptionValue('z');
+ }
+ }
+
+ @Override
+ protected void setNumber() {
+ if (commandLine.hasOption('n')) { // number
+ seqStart = Integer.parseInt(commandLine.getOptionValue('n'));
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLIScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLIScriptConfiguration.java
new file mode 100644
index 000000000000..ff79c7cfa703
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLIScriptConfiguration.java
@@ -0,0 +1,56 @@
+/**
+ * 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.itemexport;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link ItemExportCLI} script
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemExportCLIScriptConfiguration extends ItemExportScriptConfiguration {
+
+ @Override
+ public Options getOptions() {
+ Options options = new Options();
+
+ options.addOption(Option.builder("t").longOpt("type")
+ .desc("type: COLLECTION or ITEM")
+ .hasArg().required().build());
+ options.addOption(Option.builder("i").longOpt("id")
+ .desc("ID or handle of thing to export")
+ .hasArg().required().build());
+ options.addOption(Option.builder("d").longOpt("dest")
+ .desc("destination where you want items to go")
+ .hasArg().required().build());
+ options.addOption(Option.builder("n").longOpt("number")
+ .desc("sequence number to begin exporting items with")
+ .hasArg().required().build());
+ options.addOption(Option.builder("z").longOpt("zip")
+ .desc("export as zip file (specify filename e.g. export.zip)")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("m").longOpt("migrate")
+ .desc("export for migration (remove handle and metadata that will be re-created in new system)")
+ .hasArg(false).required(false).build());
+
+ // as pointed out by Peter Dietz this provides similar functionality to export metadata
+ // but it is needed since it directly exports to Simple Archive Format (SAF)
+ options.addOption(Option.builder("x").longOpt("exclude-bitstreams")
+ .desc("do not export bitstreams")
+ .hasArg(false).required(false).build());
+
+ options.addOption(Option.builder("h").longOpt("help")
+ .desc("help")
+ .hasArg(false).required(false).build());
+
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLITool.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLITool.java
deleted file mode 100644
index d6a69b582394..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportCLITool.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/**
- * 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.itemexport;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.UUID;
-
-import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.CommandLineParser;
-import org.apache.commons.cli.DefaultParser;
-import org.apache.commons.cli.HelpFormatter;
-import org.apache.commons.cli.Options;
-import org.dspace.app.itemexport.factory.ItemExportServiceFactory;
-import org.dspace.app.itemexport.service.ItemExportService;
-import org.dspace.content.Collection;
-import org.dspace.content.Item;
-import org.dspace.content.factory.ContentServiceFactory;
-import org.dspace.content.service.CollectionService;
-import org.dspace.content.service.ItemService;
-import org.dspace.core.Constants;
-import org.dspace.core.Context;
-import org.dspace.handle.factory.HandleServiceFactory;
-import org.dspace.handle.service.HandleService;
-
-/**
- * Item exporter to create simple AIPs for DSpace content. Currently exports
- * individual items, or entire collections. For instructions on use, see
- * printUsage() method.
- *
- * ItemExport creates the simple AIP package that the importer also uses. It
- * consists of:
- *
- * /exportdir/42/ (one directory per item) / dublin_core.xml - qualified dublin
- * core in RDF schema / contents - text file, listing one file per line / file1
- * - files contained in the item / file2 / ...
- *
- * issues -doesn't handle special characters in metadata (needs to turn {@code &'s} into
- * {@code &}, etc.)
- *
- * Modified by David Little, UCSD Libraries 12/21/04 to allow the registration
- * of files (bitstreams) into DSpace.
- *
- * @author David Little
- * @author Jay Paz
- */
-public class ItemExportCLITool {
-
- protected static ItemExportService itemExportService = ItemExportServiceFactory.getInstance()
- .getItemExportService();
- protected static HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
- protected static ItemService itemService = ContentServiceFactory.getInstance().getItemService();
- protected static CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService();
-
- /**
- * Default constructor
- */
- private ItemExportCLITool() { }
-
- /*
- *
- */
- public static void main(String[] argv) throws Exception {
- // create an options object and populate it
- CommandLineParser parser = new DefaultParser();
-
- Options options = new Options();
-
- options.addOption("t", "type", true, "type: COLLECTION or ITEM");
- options.addOption("i", "id", true, "ID or handle of thing to export");
- options.addOption("d", "dest", true,
- "destination where you want items to go");
- options.addOption("m", "migrate", false,
- "export for migration (remove handle and metadata that will be re-created in new system)");
- options.addOption("n", "number", true,
- "sequence number to begin exporting items with");
- options.addOption("z", "zip", true, "export as zip file (specify filename e.g. export.zip)");
- options.addOption("h", "help", false, "help");
-
- // as pointed out by Peter Dietz this provides similar functionality to export metadata
- // but it is needed since it directly exports to Simple Archive Format (SAF)
- options.addOption("x", "exclude-bitstreams", false, "do not export bitstreams");
-
- CommandLine line = parser.parse(options, argv);
-
- String typeString = null;
- String destDirName = null;
- String myIDString = null;
- int seqStart = -1;
- int myType = -1;
-
- Item myItem = null;
- Collection mycollection = null;
-
- if (line.hasOption('h')) {
- HelpFormatter myhelp = new HelpFormatter();
- myhelp.printHelp("ItemExport\n", options);
- System.out
- .println("\nfull collection: ItemExport -t COLLECTION -i ID -d dest -n number");
- System.out
- .println("singleitem: ItemExport -t ITEM -i ID -d dest -n number");
-
- System.exit(0);
- }
-
- if (line.hasOption('t')) { // type
- typeString = line.getOptionValue('t');
-
- if ("ITEM".equals(typeString)) {
- myType = Constants.ITEM;
- } else if ("COLLECTION".equals(typeString)) {
- myType = Constants.COLLECTION;
- }
- }
-
- if (line.hasOption('i')) { // id
- myIDString = line.getOptionValue('i');
- }
-
- if (line.hasOption('d')) { // dest
- destDirName = line.getOptionValue('d');
- }
-
- if (line.hasOption('n')) { // number
- seqStart = Integer.parseInt(line.getOptionValue('n'));
- }
-
- boolean migrate = false;
- if (line.hasOption('m')) { // number
- migrate = true;
- }
-
- boolean zip = false;
- String zipFileName = "";
- if (line.hasOption('z')) {
- zip = true;
- zipFileName = line.getOptionValue('z');
- }
-
- boolean excludeBitstreams = false;
- if (line.hasOption('x')) {
- excludeBitstreams = true;
- }
-
- // now validate the args
- if (myType == -1) {
- System.out
- .println("type must be either COLLECTION or ITEM (-h for help)");
- System.exit(1);
- }
-
- if (destDirName == null) {
- System.out
- .println("destination directory must be set (-h for help)");
- System.exit(1);
- }
-
- if (seqStart == -1) {
- System.out
- .println("sequence start number must be set (-h for help)");
- System.exit(1);
- }
-
- if (myIDString == null) {
- System.out
- .println("ID must be set to either a database ID or a handle (-h for help)");
- System.exit(1);
- }
-
- Context c = new Context(Context.Mode.READ_ONLY);
- c.turnOffAuthorisationSystem();
-
- if (myType == Constants.ITEM) {
- // first, is myIDString a handle?
- if (myIDString.indexOf('/') != -1) {
- myItem = (Item) handleService.resolveToObject(c, myIDString);
-
- if ((myItem == null) || (myItem.getType() != Constants.ITEM)) {
- myItem = null;
- }
- } else {
- myItem = itemService.find(c, UUID.fromString(myIDString));
- }
-
- if (myItem == null) {
- System.out
- .println("Error, item cannot be found: " + myIDString);
- }
- } else {
- if (myIDString.indexOf('/') != -1) {
- // has a / must be a handle
- mycollection = (Collection) handleService.resolveToObject(c,
- myIDString);
-
- // ensure it's a collection
- if ((mycollection == null)
- || (mycollection.getType() != Constants.COLLECTION)) {
- mycollection = null;
- }
- } else if (myIDString != null) {
- mycollection = collectionService.find(c, UUID.fromString(myIDString));
- }
-
- if (mycollection == null) {
- System.out.println("Error, collection cannot be found: "
- + myIDString);
- System.exit(1);
- }
- }
-
- if (zip) {
- Iterator- items;
- if (myItem != null) {
- List
- myItems = new ArrayList<>();
- myItems.add(myItem);
- items = myItems.iterator();
- } else {
- System.out.println("Exporting from collection: " + myIDString);
- items = itemService.findByCollection(c, mycollection);
- }
- itemExportService.exportAsZip(c, items, destDirName, zipFileName, seqStart, migrate, excludeBitstreams);
- } else {
- if (myItem != null) {
- // it's only a single item
- itemExportService
- .exportItem(c, Collections.singletonList(myItem).iterator(), destDirName, seqStart, migrate,
- excludeBitstreams);
- } else {
- System.out.println("Exporting from collection: " + myIDString);
-
- // it's a collection, so do a bunch of items
- Iterator
- i = itemService.findByCollection(c, mycollection);
- itemExportService.exportItem(c, i, destDirName, seqStart, migrate, excludeBitstreams);
- }
- }
-
- c.complete();
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java
new file mode 100644
index 000000000000..527ded5c2b59
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java
@@ -0,0 +1,67 @@
+/**
+ * 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.itemexport;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.dspace.authorize.service.AuthorizeService;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link ItemExport} script
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemExportScriptConfiguration extends ScriptConfiguration {
+
+ @Autowired
+ private AuthorizeService authorizeService;
+
+ private Class dspaceRunnableClass;
+
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableClass;
+ }
+
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableClass = dspaceRunnableClass;
+ }
+
+ @Override
+ public Options getOptions() {
+ Options options = new Options();
+
+ options.addOption(Option.builder("t").longOpt("type")
+ .desc("type: COLLECTION or ITEM")
+ .hasArg().required().build());
+ options.addOption(Option.builder("i").longOpt("id")
+ .desc("ID or handle of thing to export")
+ .hasArg().required().build());
+ options.addOption(Option.builder("n").longOpt("number")
+ .desc("sequence number to begin exporting items with")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("m").longOpt("migrate")
+ .desc("export for migration (remove handle and metadata that will be re-created in new system)")
+ .hasArg(false).required(false).build());
+
+ // as pointed out by Peter Dietz this provides similar functionality to export metadata
+ // but it is needed since it directly exports to Simple Archive Format (SAF)
+ options.addOption(Option.builder("x").longOpt("exclude-bitstreams")
+ .desc("do not export bitstreams")
+ .hasArg(false).required(false).build());
+
+ options.addOption(Option.builder("h").longOpt("help")
+ .desc("help")
+ .hasArg(false).required(false).build());
+
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
index 6578e57de2ff..a884f9b07564 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java
@@ -57,6 +57,7 @@
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.handle.service.HandleService;
+import org.dspace.scripts.handler.DSpaceRunnableHandler;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -64,17 +65,21 @@
* Item exporter to create simple AIPs for DSpace content. Currently exports
* individual items, or entire collections. For instructions on use, see
* printUsage() method.
- *
+ *
* ItemExport creates the simple AIP package that the importer also uses. It
* consists of:
- *
- * /exportdir/42/ (one directory per item) / dublin_core.xml - qualified dublin
- * core in RDF schema / contents - text file, listing one file per line / file1
- * - files contained in the item / file2 / ...
- *
+ *
{@code
+ * /exportdir/42/ (one directory per item)
+ * / dublin_core.xml - qualified dublin core in RDF schema
+ * / contents - text file, listing one file per line
+ * / file1 - files contained in the item
+ * / file2
+ * / ...
+ * }
+ *
* issues -doesn't handle special characters in metadata (needs to turn {@code &'s} into
* {@code &}, etc.)
- *
+ *
* Modified by David Little, UCSD Libraries 12/21/04 to allow the registration
* of files (bitstreams) into DSpace.
*
@@ -97,11 +102,12 @@ public class ItemExportServiceImpl implements ItemExportService {
@Autowired(required = true)
protected ConfigurationService configurationService;
-
/**
* log4j logger
*/
- private final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemExportServiceImpl.class);
+ private final Logger log = org.apache.logging.log4j.LogManager.getLogger();
+
+ private DSpaceRunnableHandler handler;
protected ItemExportServiceImpl() {
@@ -126,7 +132,7 @@ public void exportItem(Context c, Iterator- i,
}
}
- System.out.println("Beginning export");
+ logInfo("Beginning export");
while (i.hasNext()) {
if (SUBDIR_LIMIT > 0 && ++counter == SUBDIR_LIMIT) {
@@ -139,7 +145,7 @@ public void exportItem(Context c, Iterator
- i,
}
}
- System.out.println("Exporting item to " + mySequenceNumber);
+ logInfo("Exporting item to " + mySequenceNumber);
Item item = i.next();
exportItem(c, item, fullPath, mySequenceNumber, migrate, excludeBitstreams);
c.uncacheEntity(item);
@@ -155,7 +161,7 @@ protected void exportItem(Context c, Item myItem, String destDirName,
// now create a subdirectory
File itemDir = new File(destDir + "/" + seqStart);
- System.out.println("Exporting Item " + myItem.getID() +
+ logInfo("Exporting Item " + myItem.getID() +
(myItem.getHandle() != null ? ", handle " + myItem.getHandle() : "") +
" to " + itemDir);
@@ -168,6 +174,7 @@ protected void exportItem(Context c, Item myItem, String destDirName,
// make it this far, now start exporting
writeMetadata(c, myItem, itemDir, migrate);
writeBitstreams(c, myItem, itemDir, excludeBitstreams);
+ writeCollections(myItem, itemDir);
if (!migrate) {
writeHandle(c, myItem, itemDir);
}
@@ -225,7 +232,7 @@ protected void writeMetadata(Context c, String schema, Item i,
File outFile = new File(destDir, filename);
- System.out.println("Attempting to create file " + outFile);
+ logInfo("Attempting to create file " + outFile);
if (outFile.createNewFile()) {
BufferedOutputStream out = new BufferedOutputStream(
@@ -343,6 +350,33 @@ protected void writeHandle(Context c, Item i, File destDir)
}
}
+ /**
+ * Create the 'collections' file. List handles of all Collections which
+ * contain this Item. The "owning" Collection is listed first.
+ *
+ * @param item list collections holding this Item.
+ * @param destDir write the file here.
+ * @throws IOException if the file cannot be created or written.
+ */
+ protected void writeCollections(Item item, File destDir)
+ throws IOException {
+ File outFile = new File(destDir, "collections");
+ if (outFile.createNewFile()) {
+ try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) {
+ String ownerHandle = item.getOwningCollection().getHandle();
+ out.println(ownerHandle);
+ for (Collection collection : item.getCollections()) {
+ String collectionHandle = collection.getHandle();
+ if (!collectionHandle.equals(ownerHandle)) {
+ out.println(collectionHandle);
+ }
+ }
+ }
+ } else {
+ throw new IOException("Cannot create 'collections' in " + destDir);
+ }
+ }
+
/**
* Create both the bitstreams and the contents file. Any bitstreams that
* were originally registered will be marked in the contents file as such.
@@ -399,7 +433,7 @@ protected void writeBitstreams(Context c, Item i, File destDir,
File fdirs = new File(destDir + File.separator
+ dirs);
if (!fdirs.exists() && !fdirs.mkdirs()) {
- log.error("Unable to create destination directory");
+ logError("Unable to create destination directory");
}
}
@@ -456,12 +490,12 @@ public void exportAsZip(Context context, Iterator
- items,
File wkDir = new File(workDir);
if (!wkDir.exists() && !wkDir.mkdirs()) {
- log.error("Unable to create working direcory");
+ logError("Unable to create working direcory");
}
File dnDir = new File(destDirName);
if (!dnDir.exists() && !dnDir.mkdirs()) {
- log.error("Unable to create destination directory");
+ logError("Unable to create destination directory");
}
// export the items using normal export method
@@ -630,11 +664,9 @@ protected void processDownloadableExport(List dsObjects,
Thread go = new Thread() {
@Override
public void run() {
- Context context = null;
+ Context context = new Context();
Iterator
- iitems = null;
try {
- // create a new dspace context
- context = new Context();
// ignore auths
context.turnOffAuthorisationSystem();
@@ -646,7 +678,7 @@ public void run() {
String downloadDir = getExportDownloadDirectory(eperson);
File dnDir = new File(downloadDir);
if (!dnDir.exists() && !dnDir.mkdirs()) {
- log.error("Unable to create download directory");
+ logError("Unable to create download directory");
}
Iterator iter = itemsMap.keySet().iterator();
@@ -665,7 +697,7 @@ public void run() {
File wkDir = new File(workDir);
if (!wkDir.exists() && !wkDir.mkdirs()) {
- log.error("Unable to create working directory");
+ logError("Unable to create working directory");
}
@@ -756,7 +788,8 @@ public String getExportWorkDirectory() throws Exception {
throw new Exception(
"A dspace.cfg entry for 'org.dspace.app.itemexport.work.dir' does not exist.");
}
- return exportDir;
+ // clean work dir path from duplicate separators
+ return StringUtils.replace(exportDir, File.separator + File.separator, File.separator);
}
@Override
@@ -884,7 +917,7 @@ public void deleteOldExportArchives(EPerson eperson) throws Exception {
for (File file : files) {
if (file.lastModified() < now.getTimeInMillis()) {
if (!file.delete()) {
- log.error("Unable to delete export file");
+ logError("Unable to delete export file");
}
}
}
@@ -908,7 +941,7 @@ public void deleteOldExportArchives() throws Exception {
for (File file : files) {
if (file.lastModified() < now.getTimeInMillis()) {
if (!file.delete()) {
- log.error("Unable to delete old files");
+ logError("Unable to delete old files");
}
}
}
@@ -916,7 +949,7 @@ public void deleteOldExportArchives() throws Exception {
// If the directory is now empty then we delete it too.
if (dir.listFiles().length == 0) {
if (!dir.delete()) {
- log.error("Unable to delete directory");
+ logError("Unable to delete directory");
}
}
}
@@ -937,14 +970,14 @@ public void emailSuccessMessage(Context context, EPerson eperson,
email.send();
} catch (Exception e) {
- log.warn(LogHelper.getHeader(context, "emailSuccessMessage", "cannot notify user of export"), e);
+ logWarn(LogHelper.getHeader(context, "emailSuccessMessage", "cannot notify user of export"), e);
}
}
@Override
public void emailErrorMessage(EPerson eperson, String error)
throws MessagingException {
- log.warn("An error occurred during item export, the user will be notified. " + error);
+ logWarn("An error occurred during item export, the user will be notified. " + error);
try {
Locale supportedLocale = I18nUtil.getEPersonLocale(eperson);
Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "export_error"));
@@ -954,7 +987,7 @@ public void emailErrorMessage(EPerson eperson, String error)
email.send();
} catch (Exception e) {
- log.warn("error during item export error notification", e);
+ logWarn("error during item export error notification", e);
}
}
@@ -969,7 +1002,7 @@ public void zip(String strSource, String target) throws Exception {
}
File targetFile = new File(tempFileName);
if (!targetFile.createNewFile()) {
- log.warn("Target file already exists: " + targetFile.getName());
+ logWarn("Target file already exists: " + targetFile.getName());
}
FileOutputStream fos = new FileOutputStream(tempFileName);
@@ -985,7 +1018,7 @@ public void zip(String strSource, String target) throws Exception {
deleteDirectory(cpFile);
if (!targetFile.renameTo(new File(target))) {
- log.error("Unable to rename file");
+ logError("Unable to rename file");
}
} finally {
if (cpZipOutputStream != null) {
@@ -1018,8 +1051,11 @@ protected void zipFiles(File cpFile, String strSource,
return;
}
String strAbsPath = cpFile.getPath();
- String strZipEntryName = strAbsPath.substring(strSource
- .length() + 1, strAbsPath.length());
+ int startIndex = strSource.length();
+ if (!StringUtils.endsWith(strSource, File.separator)) {
+ startIndex++;
+ }
+ String strZipEntryName = strAbsPath.substring(startIndex, strAbsPath.length());
// byte[] b = new byte[ (int)(cpFile.length()) ];
@@ -1058,7 +1094,7 @@ protected boolean deleteDirectory(File path) {
deleteDirectory(file);
} else {
if (!file.delete()) {
- log.error("Unable to delete file: " + file.getName());
+ logError("Unable to delete file: " + file.getName());
}
}
}
@@ -1067,4 +1103,64 @@ protected boolean deleteDirectory(File path) {
return (path.delete());
}
+ @Override
+ public void setHandler(DSpaceRunnableHandler handler) {
+ this.handler = handler;
+ }
+
+ private void logInfo(String message) {
+ logInfo(message, null);
+ }
+
+ private void logInfo(String message, Exception e) {
+ if (handler != null) {
+ handler.logInfo(message);
+ return;
+ }
+
+ if (e != null) {
+ log.info(message, e);
+ } else {
+ log.info(message);
+ }
+ }
+
+ private void logWarn(String message) {
+ logWarn(message, null);
+ }
+
+ private void logWarn(String message, Exception e) {
+ if (handler != null) {
+ handler.logWarning(message);
+ return;
+ }
+
+ if (e != null) {
+ log.warn(message, e);
+ } else {
+ log.warn(message);
+ }
+ }
+
+ private void logError(String message) {
+ logError(message, null);
+ }
+
+ private void logError(String message, Exception e) {
+ if (handler != null) {
+ if (e != null) {
+ handler.logError(message, e);
+ } else {
+ handler.logError(message);
+ }
+ return;
+ }
+
+ if (e != null) {
+ log.error(message, e);
+ } else {
+ log.error(message);
+ }
+ }
+
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java b/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
index 7dedc9950b4f..6ec1027709bb 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemexport/service/ItemExportService.java
@@ -17,6 +17,7 @@
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
+import org.dspace.scripts.handler.DSpaceRunnableHandler;
/**
* Item exporter to create simple AIPs for DSpace content. Currently exports
@@ -267,4 +268,10 @@ public void emailErrorMessage(EPerson eperson, String error)
*/
public void zip(String strSource, String target) throws Exception;
+ /**
+ * Set the DSpace Runnable Handler
+ * @param handler
+ */
+ public void setHandler(DSpaceRunnableHandler handler);
+
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java
new file mode 100644
index 000000000000..b32de11f7a7f
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java
@@ -0,0 +1,440 @@
+/**
+ * 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;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.tika.Tika;
+import org.dspace.app.itemimport.factory.ItemImportServiceFactory;
+import org.dspace.app.itemimport.service.ItemImportService;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.content.Collection;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.CollectionService;
+import org.dspace.core.Constants;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.dspace.eperson.factory.EPersonServiceFactory;
+import org.dspace.eperson.service.EPersonService;
+import org.dspace.handle.factory.HandleServiceFactory;
+import org.dspace.handle.service.HandleService;
+import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.utils.DSpace;
+
+/**
+ * Import items into DSpace. The conventional use is upload files by copying
+ * them. DSpace writes the item's bitstreams into its assetstore. Metadata is
+ * also loaded to the DSpace database.
+ *
+ * A second use assumes the bitstream files already exist in a storage
+ * resource accessible to DSpace. In this case the bitstreams are 'registered'.
+ * That is, the metadata is loaded to the DSpace database and DSpace is given
+ * the location of the file which is subsumed into DSpace.
+ *
+ * The distinction is controlled by the format of lines in the 'contents' file.
+ * See comments in processContentsFile() below.
+ *
+ * Modified by David Little, UCSD Libraries 12/21/04 to
+ * allow the registration of files (bitstreams) into DSpace.
+ */
+public class ItemImport extends DSpaceRunnable {
+
+ public static String TEMP_DIR = "importSAF";
+ public static String MAPFILE_FILENAME = "mapfile";
+ public static String MAPFILE_BITSTREAM_TYPE = "importSAFMapfile";
+
+ protected boolean template = false;
+ protected String command = null;
+ protected String sourcedir = null;
+ protected String mapfile = null;
+ protected String eperson = null;
+ protected String[] collections = null;
+ protected boolean isTest = false;
+ protected boolean isExcludeContent = false;
+ protected boolean isResume = false;
+ protected boolean useWorkflow = false;
+ protected boolean useWorkflowSendEmail = false;
+ protected boolean isQuiet = false;
+ protected boolean commandLineCollections = false;
+ protected boolean zip = false;
+ protected boolean remoteUrl = false;
+ protected String zipfilename = null;
+ protected boolean zipvalid = false;
+ protected boolean help = false;
+ protected File workDir = null;
+ protected File workFile = null;
+
+ protected static final CollectionService collectionService =
+ ContentServiceFactory.getInstance().getCollectionService();
+ protected static final EPersonService epersonService =
+ EPersonServiceFactory.getInstance().getEPersonService();
+ protected static final HandleService handleService =
+ HandleServiceFactory.getInstance().getHandleService();
+
+ @Override
+ public ItemImportScriptConfiguration getScriptConfiguration() {
+ return new DSpace().getServiceManager()
+ .getServiceByName("import", ItemImportScriptConfiguration.class);
+ }
+
+ @Override
+ public void setup() throws ParseException {
+ help = commandLine.hasOption('h');
+
+ if (commandLine.hasOption('a')) {
+ command = "add";
+ }
+
+ if (commandLine.hasOption('r')) {
+ command = "replace";
+ }
+
+ if (commandLine.hasOption('d')) {
+ command = "delete";
+ }
+
+ if (commandLine.hasOption('w')) {
+ useWorkflow = true;
+ if (commandLine.hasOption('n')) {
+ useWorkflowSendEmail = true;
+ }
+ }
+
+ if (commandLine.hasOption('v')) {
+ isTest = true;
+ handler.logInfo("**Test Run** - not actually importing items.");
+ }
+
+ isExcludeContent = commandLine.hasOption('x');
+
+ if (commandLine.hasOption('p')) {
+ template = true;
+ }
+
+ if (commandLine.hasOption('c')) { // collections
+ collections = commandLine.getOptionValues('c');
+ commandLineCollections = true;
+ } else {
+ handler.logInfo("No collections given. Assuming 'collections' file inside item directory");
+ }
+
+ if (commandLine.hasOption('R')) {
+ isResume = true;
+ handler.logInfo("**Resume import** - attempting to import items not already imported");
+ }
+
+ if (commandLine.hasOption('q')) {
+ isQuiet = true;
+ }
+
+ setZip();
+ }
+
+ @Override
+ public void internalRun() throws Exception {
+ if (help) {
+ printHelp();
+ return;
+ }
+
+ Date startTime = new Date();
+ Context context = new Context(Context.Mode.BATCH_EDIT);
+
+ setMapFile();
+
+ validate(context);
+
+ setEPerson(context);
+
+ // check collection
+ List mycollections = null;
+ // don't need to validate collections set if command is "delete"
+ // also if no collections are given in the command line
+ if (!"delete".equals(command) && commandLineCollections) {
+ handler.logInfo("Destination collections:");
+
+ mycollections = new ArrayList<>();
+
+ // validate each collection arg to see if it's a real collection
+ for (int i = 0; i < collections.length; i++) {
+ Collection collection = null;
+ if (collections[i] != null) {
+ // is the ID a handle?
+ if (collections[i].indexOf('/') != -1) {
+ // string has a / so it must be a handle - try and resolve
+ // it
+ collection = ((Collection) handleService
+ .resolveToObject(context, collections[i]));
+ } else {
+ // not a handle, try and treat it as an integer collection database ID
+ collection = collectionService.find(context, UUID.fromString(collections[i]));
+ }
+ }
+
+ // was the collection valid?
+ if (collection == null
+ || collection.getType() != Constants.COLLECTION) {
+ throw new IllegalArgumentException("Cannot resolve "
+ + collections[i] + " to collection");
+ }
+
+ // add resolved collection to list
+ mycollections.add(collection);
+
+ // print progress info
+ handler.logInfo((i == 0 ? "Owning " : "") + "Collection: " + collection.getName());
+ }
+ }
+ // end validation
+
+ // start
+ ItemImportService itemImportService = ItemImportServiceFactory.getInstance()
+ .getItemImportService();
+ try {
+ itemImportService.setTest(isTest);
+ itemImportService.setExcludeContent(isExcludeContent);
+ itemImportService.setResume(isResume);
+ itemImportService.setUseWorkflow(useWorkflow);
+ itemImportService.setUseWorkflowSendEmail(useWorkflowSendEmail);
+ itemImportService.setQuiet(isQuiet);
+ itemImportService.setHandler(handler);
+
+ try {
+ context.turnOffAuthorisationSystem();
+
+ readZip(context, itemImportService);
+
+ process(context, itemImportService, mycollections);
+
+ // complete all transactions
+ context.complete();
+ } catch (Exception e) {
+ context.abort();
+ throw new Exception(
+ "Error committing changes to database: " + e.getMessage() + ", aborting most recent changes", e);
+ }
+
+ if (isTest) {
+ handler.logInfo("***End of Test Run***");
+ }
+ } finally {
+ if (zip) {
+ // if zip file was valid then clean sourcedir
+ if (zipvalid && sourcedir != null && new File(sourcedir).exists()) {
+ FileUtils.deleteDirectory(new File(sourcedir));
+ }
+
+ // clean workdir
+ if (workDir != null && workDir.exists()) {
+ FileUtils.deleteDirectory(workDir);
+ }
+
+ // conditionally clean workFile if import was done in the UI or via a URL and it still exists
+ if (workFile != null && workFile.exists()) {
+ workFile.delete();
+ }
+ }
+
+ Date endTime = new Date();
+ handler.logInfo("Started: " + startTime.getTime());
+ handler.logInfo("Ended: " + endTime.getTime());
+ handler.logInfo(
+ "Elapsed time: " + ((endTime.getTime() - startTime.getTime()) / 1000) + " secs (" + (endTime
+ .getTime() - startTime.getTime()) + " msecs)");
+ }
+ }
+
+ /**
+ * Validate the options
+ * @param context
+ */
+ protected void validate(Context context) {
+ // check zip type: uploaded file or remote url
+ if (commandLine.hasOption('z')) {
+ zipfilename = commandLine.getOptionValue('z');
+ } else if (commandLine.hasOption('u')) {
+ remoteUrl = true;
+ zipfilename = commandLine.getOptionValue('u');
+ }
+ if (StringUtils.isBlank(zipfilename)) {
+ throw new UnsupportedOperationException("Must run with either name of zip file or url of zip file");
+ }
+
+ if (command == null) {
+ handler.logError("Must run with either add, replace, or remove (run with -h flag for details)");
+ throw new UnsupportedOperationException("Must run with either add, replace, or remove");
+ }
+
+ // can only resume for adds
+ if (isResume && !"add".equals(command)) {
+ handler.logError("Resume option only works with the --add command (run with -h flag for details)");
+ throw new UnsupportedOperationException("Resume option only works with the --add command");
+ }
+
+ if (isResume && StringUtils.isBlank(mapfile)) {
+ handler.logError("The mapfile does not exist. ");
+ throw new UnsupportedOperationException("The mapfile does not exist");
+ }
+ }
+
+ /**
+ * Process the import
+ * @param context
+ * @param itemImportService
+ * @param collections
+ * @throws Exception
+ */
+ protected void process(Context context, ItemImportService itemImportService,
+ List collections) throws Exception {
+ readMapfile(context);
+
+ if ("add".equals(command)) {
+ itemImportService.addItems(context, collections, sourcedir, mapfile, template);
+ } else if ("replace".equals(command)) {
+ itemImportService.replaceItems(context, collections, sourcedir, mapfile, template);
+ } else if ("delete".equals(command)) {
+ itemImportService.deleteItems(context, mapfile);
+ }
+
+ // write input stream on handler
+ File mapFile = new File(mapfile);
+ try (InputStream mapfileInputStream = new FileInputStream(mapFile)) {
+ handler.writeFilestream(context, MAPFILE_FILENAME, mapfileInputStream, MAPFILE_BITSTREAM_TYPE);
+ } finally {
+ mapFile.delete();
+ }
+ }
+
+ /**
+ * Read the ZIP archive in SAF format
+ * @param context
+ * @param itemImportService
+ * @throws Exception
+ */
+ protected void readZip(Context context, ItemImportService itemImportService) throws Exception {
+ Optional optionalFileStream = Optional.empty();
+ Optional validationFileStream = Optional.empty();
+ if (!remoteUrl) {
+ // manage zip via upload
+ optionalFileStream = handler.getFileStream(context, zipfilename);
+ validationFileStream = handler.getFileStream(context, zipfilename);
+ } else {
+ // manage zip via remote url
+ optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
+ validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
+ }
+
+ if (validationFileStream.isPresent()) {
+ // validate zip file
+ if (validationFileStream.isPresent()) {
+ validateZip(validationFileStream.get());
+ }
+
+ workFile = new File(itemImportService.getTempWorkDir() + File.separator
+ + zipfilename + "-" + context.getCurrentUser().getID());
+ FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile);
+ } else {
+ throw new IllegalArgumentException(
+ "Error reading file, the file couldn't be found for filename: " + zipfilename);
+ }
+
+ workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ + File.separator + context.getCurrentUser().getID());
+ sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath());
+ }
+
+ /**
+ * Confirm that the zip file has the correct MIME type
+ * @param inputStream
+ */
+ protected void validateZip(InputStream inputStream) {
+ Tika tika = new Tika();
+ try {
+ String mimeType = tika.detect(inputStream);
+ if (mimeType.equals("application/zip")) {
+ zipvalid = true;
+ } else {
+ handler.logError("A valid zip file must be supplied. The provided file has mimetype: " + mimeType);
+ throw new UnsupportedOperationException("A valid zip file must be supplied");
+ }
+ } catch (IOException e) {
+ throw new IllegalArgumentException(
+ "There was an error while reading the zip file: " + zipfilename);
+ }
+ }
+
+ /**
+ * Read the mapfile
+ * @param context
+ */
+ protected void readMapfile(Context context) {
+ if (isResume) {
+ try {
+ Optional optionalFileStream = handler.getFileStream(context, mapfile);
+ if (optionalFileStream.isPresent()) {
+ File tempFile = File.createTempFile(mapfile, "temp");
+ tempFile.deleteOnExit();
+ FileUtils.copyInputStreamToFile(optionalFileStream.get(), tempFile);
+ mapfile = tempFile.getAbsolutePath();
+ }
+ } catch (IOException | AuthorizeException e) {
+ throw new UnsupportedOperationException("The mapfile does not exist");
+ }
+ }
+ }
+
+ /**
+ * Set the mapfile option
+ * @throws IOException
+ */
+ protected void setMapFile() throws IOException {
+ if (isResume && commandLine.hasOption('m')) {
+ mapfile = commandLine.getOptionValue('m');
+ } else {
+ mapfile = Files.createTempFile(MAPFILE_FILENAME, "temp").toString();
+ }
+ }
+
+ /**
+ * Set the zip option
+ */
+ protected void setZip() {
+ zip = true;
+ }
+
+ /**
+ * Set the eperson in the context
+ * @param context
+ * @throws SQLException
+ */
+ protected void setEPerson(Context context) throws SQLException {
+ EPerson myEPerson = epersonService.find(context, this.getEpersonIdentifier());
+
+ // check eperson
+ if (myEPerson == null) {
+ handler.logError("EPerson cannot be found: " + this.getEpersonIdentifier());
+ throw new UnsupportedOperationException("EPerson cannot be found: " + this.getEpersonIdentifier());
+ }
+
+ context.setCurrentUser(myEPerson);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java
new file mode 100644
index 000000000000..98d2469b7155
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java
@@ -0,0 +1,187 @@
+/**
+ * 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;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.itemimport.service.ItemImportService;
+import org.dspace.content.Collection;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+
+/**
+ * CLI variant for the {@link ItemImport} class.
+ * This was done to specify the specific behaviors for the CLI.
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemImportCLI extends ItemImport {
+
+ @Override
+ protected void validate(Context context) {
+ // can only resume for adds
+ if (isResume && !"add".equals(command)) {
+ handler.logError("Resume option only works with the --add command (run with -h flag for details)");
+ throw new UnsupportedOperationException("Resume option only works with the --add command");
+ }
+
+ if (commandLine.hasOption('e')) {
+ eperson = commandLine.getOptionValue('e');
+ }
+
+ // check eperson identifier (email or id)
+ if (eperson == null) {
+ handler.logError("An eperson to do the importing must be specified (run with -h flag for details)");
+ throw new UnsupportedOperationException("An eperson to do the importing must be specified");
+ }
+
+ File myFile = null;
+ try {
+ myFile = new File(mapfile);
+ } catch (Exception e) {
+ throw new UnsupportedOperationException("The mapfile " + mapfile + " does not exist");
+ }
+
+ if (!isResume && "add".equals(command) && myFile.exists()) {
+ handler.logError("The mapfile " + mapfile + " already exists. "
+ + "Either delete it or use --resume if attempting to resume an aborted import. "
+ + "(run with -h flag for details)");
+ throw new UnsupportedOperationException("The mapfile " + mapfile + " already exists");
+ }
+
+ if (command == null) {
+ handler.logError("Must run with either add, replace, or remove (run with -h flag for details)");
+ throw new UnsupportedOperationException("Must run with either add, replace, or remove");
+ } else if ("add".equals(command) || "replace".equals(command)) {
+ if (!remoteUrl && sourcedir == null) {
+ handler.logError("A source directory containing items must be set (run with -h flag for details)");
+ throw new UnsupportedOperationException("A source directory containing items must be set");
+ }
+
+ if (mapfile == null) {
+ handler.logError(
+ "A map file to hold importing results must be specified (run with -h flag for details)");
+ throw new UnsupportedOperationException("A map file to hold importing results must be specified");
+ }
+ } else if ("delete".equals(command)) {
+ if (mapfile == null) {
+ handler.logError("A map file must be specified (run with -h flag for details)");
+ throw new UnsupportedOperationException("A map file must be specified");
+ }
+ }
+ }
+
+ @Override
+ protected void process(Context context, ItemImportService itemImportService,
+ List collections) throws Exception {
+ if ("add".equals(command)) {
+ itemImportService.addItems(context, collections, sourcedir, mapfile, template);
+ } else if ("replace".equals(command)) {
+ itemImportService.replaceItems(context, collections, sourcedir, mapfile, template);
+ } else if ("delete".equals(command)) {
+ itemImportService.deleteItems(context, mapfile);
+ }
+ }
+
+ @Override
+ protected void readZip(Context context, ItemImportService itemImportService) throws Exception {
+ // If this is a zip archive, unzip it first
+ if (zip) {
+ if (!remoteUrl) {
+ // confirm zip file exists
+ File myZipFile = new File(sourcedir + File.separator + zipfilename);
+ if ((!myZipFile.exists()) || (!myZipFile.isFile())) {
+ throw new IllegalArgumentException(
+ "Error reading file, the file couldn't be found for filename: " + zipfilename);
+ }
+
+ // validate zip file
+ InputStream validationFileStream = new FileInputStream(myZipFile);
+ validateZip(validationFileStream);
+
+ workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ + File.separator + context.getCurrentUser().getID());
+ sourcedir = itemImportService.unzip(
+ new File(sourcedir + File.separator + zipfilename), workDir.getAbsolutePath());
+ } else {
+ // manage zip via remote url
+ Optional optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
+ if (optionalFileStream.isPresent()) {
+ // validate zip file via url
+ Optional validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream());
+ if (validationFileStream.isPresent()) {
+ validateZip(validationFileStream.get());
+ }
+
+ workFile = new File(itemImportService.getTempWorkDir() + File.separator
+ + zipfilename + "-" + context.getCurrentUser().getID());
+ FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile);
+ workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR
+ + File.separator + context.getCurrentUser().getID());
+ sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath());
+ } else {
+ throw new IllegalArgumentException(
+ "Error reading file, the file couldn't be found for filename: " + zipfilename);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void setMapFile() {
+ if (commandLine.hasOption('m')) {
+ mapfile = commandLine.getOptionValue('m');
+ }
+ }
+
+ @Override
+ protected void setZip() {
+ if (commandLine.hasOption('s')) { // source
+ sourcedir = commandLine.getOptionValue('s');
+ }
+
+ if (commandLine.hasOption('z')) {
+ zip = true;
+ zipfilename = commandLine.getOptionValue('z');
+ }
+
+ if (commandLine.hasOption('u')) { // remote url
+ zip = true;
+ remoteUrl = true;
+ zipfilename = commandLine.getOptionValue('u');
+ }
+ }
+
+ @Override
+ protected void setEPerson(Context context) throws SQLException {
+ EPerson myEPerson = null;
+ if (StringUtils.contains(eperson, '@')) {
+ // @ sign, must be an email
+ myEPerson = epersonService.findByEmail(context, eperson);
+ } else {
+ myEPerson = epersonService.find(context, UUID.fromString(eperson));
+ }
+
+ // check eperson
+ if (myEPerson == null) {
+ handler.logError("EPerson cannot be found: " + eperson + " (run with -h flag for details)");
+ throw new UnsupportedOperationException("EPerson cannot be found: " + eperson);
+ }
+
+ context.setCurrentUser(myEPerson);
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLIScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLIScriptConfiguration.java
new file mode 100644
index 000000000000..89abd7155b39
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLIScriptConfiguration.java
@@ -0,0 +1,80 @@
+/**
+ * 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;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link ItemImportCLI} script
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemImportCLIScriptConfiguration extends ItemImportScriptConfiguration {
+
+ @Override
+ public Options getOptions() {
+ Options options = new Options();
+
+ options.addOption(Option.builder("a").longOpt("add")
+ .desc("add items to DSpace")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("r").longOpt("replace")
+ .desc("replace items in mapfile")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("d").longOpt("delete")
+ .desc("delete items listed in mapfile")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("s").longOpt("source")
+ .desc("source of items (directory)")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("z").longOpt("zip")
+ .desc("name of zip file")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("u").longOpt("url")
+ .desc("url of zip file")
+ .hasArg().build());
+ options.addOption(Option.builder("c").longOpt("collection")
+ .desc("destination collection(s) Handle or database ID")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("m").longOpt("mapfile")
+ .desc("mapfile items in mapfile")
+ .hasArg().required().build());
+ options.addOption(Option.builder("e").longOpt("eperson")
+ .desc("email of eperson doing importing")
+ .hasArg().required().build());
+ options.addOption(Option.builder("w").longOpt("workflow")
+ .desc("send submission through collection's workflow")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("n").longOpt("notify")
+ .desc("if sending submissions through the workflow, send notification emails")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("v").longOpt("validate")
+ .desc("test run - do not actually import items")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("x").longOpt("exclude-bitstreams")
+ .desc("do not load or expect content bitstreams")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("p").longOpt("template")
+ .desc("apply template")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("R").longOpt("resume")
+ .desc("resume a failed import (add only)")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("q").longOpt("quiet")
+ .desc("don't display metadata")
+ .hasArg(false).required(false).build());
+
+ options.addOption(Option.builder("h").longOpt("help")
+ .desc("help")
+ .hasArg(false).required(false).build());
+
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLITool.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLITool.java
deleted file mode 100644
index afee478f9cfd..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLITool.java
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * 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;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
-
-import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.CommandLineParser;
-import org.apache.commons.cli.DefaultParser;
-import org.apache.commons.cli.HelpFormatter;
-import org.apache.commons.cli.Options;
-import org.dspace.app.itemimport.factory.ItemImportServiceFactory;
-import org.dspace.app.itemimport.service.ItemImportService;
-import org.dspace.content.Collection;
-import org.dspace.content.factory.ContentServiceFactory;
-import org.dspace.content.service.CollectionService;
-import org.dspace.core.Constants;
-import org.dspace.core.Context;
-import org.dspace.eperson.EPerson;
-import org.dspace.eperson.factory.EPersonServiceFactory;
-import org.dspace.eperson.service.EPersonService;
-import org.dspace.handle.factory.HandleServiceFactory;
-import org.dspace.handle.service.HandleService;
-
-/**
- * Import items into DSpace. The conventional use is upload files by copying
- * them. DSpace writes the item's bitstreams into its assetstore. Metadata is
- * also loaded to the DSpace database.
- *
- * A second use assumes the bitstream files already exist in a storage
- * resource accessible to DSpace. In this case the bitstreams are 'registered'.
- * That is, the metadata is loaded to the DSpace database and DSpace is given
- * the location of the file which is subsumed into DSpace.
- *
- * The distinction is controlled by the format of lines in the 'contents' file.
- * See comments in processContentsFile() below.
- *
- * Modified by David Little, UCSD Libraries 12/21/04 to
- * allow the registration of files (bitstreams) into DSpace.
- */
-public class ItemImportCLITool {
-
- private static boolean template = false;
-
- private static final CollectionService collectionService = ContentServiceFactory.getInstance()
- .getCollectionService();
- private static final EPersonService epersonService = EPersonServiceFactory.getInstance().getEPersonService();
- private static final HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
-
- /**
- * Default constructor
- */
- private ItemImportCLITool() { }
-
- public static void main(String[] argv) throws Exception {
- Date startTime = new Date();
- int status = 0;
-
- try {
- // create an options object and populate it
- CommandLineParser parser = new DefaultParser();
-
- Options options = new Options();
-
- options.addOption("a", "add", false, "add items to DSpace");
- options.addOption("r", "replace", false, "replace items in mapfile");
- options.addOption("d", "delete", false,
- "delete items listed in mapfile");
- options.addOption("s", "source", true, "source of items (directory)");
- options.addOption("z", "zip", true, "name of zip file");
- options.addOption("c", "collection", true,
- "destination collection(s) Handle or database ID");
- options.addOption("m", "mapfile", true, "mapfile items in mapfile");
- options.addOption("e", "eperson", true,
- "email of eperson doing importing");
- options.addOption("w", "workflow", false,
- "send submission through collection's workflow");
- options.addOption("n", "notify", false,
- "if sending submissions through the workflow, send notification emails");
- options.addOption("t", "test", false,
- "test run - do not actually import items");
- options.addOption("p", "template", false, "apply template");
- options.addOption("R", "resume", false,
- "resume a failed import (add only)");
- options.addOption("q", "quiet", false, "don't display metadata");
-
- options.addOption("h", "help", false, "help");
-
- CommandLine line = parser.parse(options, argv);
-
- String command = null; // add replace remove, etc
- String sourcedir = null;
- String mapfile = null;
- String eperson = null; // db ID or email
- String[] collections = null; // db ID or handles
- boolean isTest = false;
- boolean isResume = false;
- boolean useWorkflow = false;
- boolean useWorkflowSendEmail = false;
- boolean isQuiet = false;
-
- if (line.hasOption('h')) {
- HelpFormatter myhelp = new HelpFormatter();
- myhelp.printHelp("ItemImport\n", options);
- System.out
- .println("\nadding items: ItemImport -a -e eperson -c collection -s sourcedir -m mapfile");
- System.out
- .println(
- "\nadding items from zip file: ItemImport -a -e eperson -c collection -s sourcedir -z " +
- "filename.zip -m mapfile");
- System.out
- .println("replacing items: ItemImport -r -e eperson -c collection -s sourcedir -m mapfile");
- System.out
- .println("deleting items: ItemImport -d -e eperson -m mapfile");
- System.out
- .println(
- "If multiple collections are specified, the first collection will be the one that owns the " +
- "item.");
-
- System.exit(0);
- }
-
- if (line.hasOption('a')) {
- command = "add";
- }
-
- if (line.hasOption('r')) {
- command = "replace";
- }
-
- if (line.hasOption('d')) {
- command = "delete";
- }
-
- if (line.hasOption('w')) {
- useWorkflow = true;
- if (line.hasOption('n')) {
- useWorkflowSendEmail = true;
- }
- }
-
- if (line.hasOption('t')) {
- isTest = true;
- System.out.println("**Test Run** - not actually importing items.");
- }
-
- if (line.hasOption('p')) {
- template = true;
- }
-
- if (line.hasOption('s')) { // source
- sourcedir = line.getOptionValue('s');
- }
-
- if (line.hasOption('m')) { // mapfile
- mapfile = line.getOptionValue('m');
- }
-
- if (line.hasOption('e')) { // eperson
- eperson = line.getOptionValue('e');
- }
-
- if (line.hasOption('c')) { // collections
- collections = line.getOptionValues('c');
- }
-
- if (line.hasOption('R')) {
- isResume = true;
- System.out
- .println("**Resume import** - attempting to import items not already imported");
- }
-
- if (line.hasOption('q')) {
- isQuiet = true;
- }
-
- boolean zip = false;
- String zipfilename = "";
- if (line.hasOption('z')) {
- zip = true;
- zipfilename = line.getOptionValue('z');
- }
-
- //By default assume collections will be given on the command line
- boolean commandLineCollections = true;
- // now validate
- // must have a command set
- if (command == null) {
- System.out
- .println("Error - must run with either add, replace, or remove (run with -h flag for details)");
- System.exit(1);
- } else if ("add".equals(command) || "replace".equals(command)) {
- if (sourcedir == null) {
- System.out
- .println("Error - a source directory containing items must be set");
- System.out.println(" (run with -h flag for details)");
- System.exit(1);
- }
-
- if (mapfile == null) {
- System.out
- .println("Error - a map file to hold importing results must be specified");
- System.out.println(" (run with -h flag for details)");
- System.exit(1);
- }
-
- if (eperson == null) {
- System.out
- .println("Error - an eperson to do the importing must be specified");
- System.out.println(" (run with -h flag for details)");
- System.exit(1);
- }
-
- if (collections == null) {
- System.out.println("No collections given. Assuming 'collections' file inside item directory");
- commandLineCollections = false;
- }
- } else if ("delete".equals(command)) {
- if (eperson == null) {
- System.out
- .println("Error - an eperson to do the importing must be specified");
- System.exit(1);
- }
-
- if (mapfile == null) {
- System.out.println("Error - a map file must be specified");
- System.exit(1);
- }
- }
-
- // can only resume for adds
- if (isResume && !"add".equals(command)) {
- System.out
- .println("Error - resume option only works with the --add command");
- System.exit(1);
- }
-
- // do checks around mapfile - if mapfile exists and 'add' is selected,
- // resume must be chosen
- File myFile = new File(mapfile);
-
- if (!isResume && "add".equals(command) && myFile.exists()) {
- System.out.println("Error - the mapfile " + mapfile
- + " already exists.");
- System.out
- .println("Either delete it or use --resume if attempting to resume an aborted import.");
- System.exit(1);
- }
-
- ItemImportService myloader = ItemImportServiceFactory.getInstance().getItemImportService();
- myloader.setTest(isTest);
- myloader.setResume(isResume);
- myloader.setUseWorkflow(useWorkflow);
- myloader.setUseWorkflowSendEmail(useWorkflowSendEmail);
- myloader.setQuiet(isQuiet);
-
- // create a context
- Context c = new Context(Context.Mode.BATCH_EDIT);
-
- // find the EPerson, assign to context
- EPerson myEPerson = null;
-
- if (eperson.indexOf('@') != -1) {
- // @ sign, must be an email
- myEPerson = epersonService.findByEmail(c, eperson);
- } else {
- myEPerson = epersonService.find(c, UUID.fromString(eperson));
- }
-
- if (myEPerson == null) {
- System.out.println("Error, eperson cannot be found: " + eperson);
- System.exit(1);
- }
-
- c.setCurrentUser(myEPerson);
-
- // find collections
- List mycollections = null;
-
- // don't need to validate collections set if command is "delete"
- // also if no collections are given in the command line
- if (!"delete".equals(command) && commandLineCollections) {
- System.out.println("Destination collections:");
-
- mycollections = new ArrayList<>();
-
- // validate each collection arg to see if it's a real collection
- for (int i = 0; i < collections.length; i++) {
-
- Collection resolved = null;
-
- if (collections[i] != null) {
-
- // is the ID a handle?
- if (collections[i].indexOf('/') != -1) {
- // string has a / so it must be a handle - try and resolve
- // it
- resolved = ((Collection) handleService
- .resolveToObject(c, collections[i]));
-
- } else {
- // not a handle, try and treat it as an integer collection database ID
- resolved = collectionService.find(c, UUID.fromString(collections[i]));
-
- }
-
- }
-
- // was the collection valid?
- if ((resolved == null)
- || (resolved.getType() != Constants.COLLECTION)) {
- throw new IllegalArgumentException("Cannot resolve "
- + collections[i] + " to collection");
- }
-
- // add resolved collection to list
- mycollections.add(resolved);
-
- // print progress info
- String owningPrefix = "";
-
- if (i == 0) {
- owningPrefix = "Owning ";
- }
-
- System.out.println(owningPrefix + " Collection: "
- + resolved.getName());
- }
- } // end of validating collections
-
- try {
- // If this is a zip archive, unzip it first
- if (zip) {
- sourcedir = myloader.unzip(sourcedir, zipfilename);
- }
-
-
- c.turnOffAuthorisationSystem();
-
- if ("add".equals(command)) {
- myloader.addItems(c, mycollections, sourcedir, mapfile, template);
- } else if ("replace".equals(command)) {
- myloader.replaceItems(c, mycollections, sourcedir, mapfile, template);
- } else if ("delete".equals(command)) {
- myloader.deleteItems(c, mapfile);
- }
-
- // complete all transactions
- c.complete();
- } catch (Exception e) {
- c.abort();
- e.printStackTrace();
- System.out.println(e);
- status = 1;
- }
-
- // Delete the unzipped file
- try {
- if (zip) {
- System.gc();
- System.out.println(
- "Deleting temporary zip directory: " + myloader.getTempWorkDirFile().getAbsolutePath());
- myloader.cleanupZipTemp();
- }
- } catch (IOException ex) {
- System.out.println("Unable to delete temporary zip archive location: " + myloader.getTempWorkDirFile()
- .getAbsolutePath());
- }
-
-
- if (isTest) {
- System.out.println("***End of Test Run***");
- }
- } finally {
- Date endTime = new Date();
- System.out.println("Started: " + startTime.getTime());
- System.out.println("Ended: " + endTime.getTime());
- System.out.println(
- "Elapsed time: " + ((endTime.getTime() - startTime.getTime()) / 1000) + " secs (" + (endTime
- .getTime() - startTime.getTime()) + " msecs)");
- }
-
- System.exit(status);
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java
new file mode 100644
index 000000000000..3f2675ea58f1
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java
@@ -0,0 +1,90 @@
+/**
+ * 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;
+
+import java.io.InputStream;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.dspace.scripts.configuration.ScriptConfiguration;
+
+/**
+ * The {@link ScriptConfiguration} for the {@link ItemImport} script
+ *
+ * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com)
+ */
+public class ItemImportScriptConfiguration extends ScriptConfiguration {
+
+ private Class dspaceRunnableClass;
+
+ @Override
+ public Class getDspaceRunnableClass() {
+ return dspaceRunnableClass;
+ }
+
+ @Override
+ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
+ this.dspaceRunnableClass = dspaceRunnableClass;
+ }
+
+ @Override
+ public Options getOptions() {
+ Options options = new Options();
+
+ options.addOption(Option.builder("a").longOpt("add")
+ .desc("add items to DSpace")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("r").longOpt("replace")
+ .desc("replace items in mapfile")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("d").longOpt("delete")
+ .desc("delete items listed in mapfile")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("z").longOpt("zip")
+ .desc("name of zip file")
+ .type(InputStream.class)
+ .hasArg().build());
+ options.addOption(Option.builder("u").longOpt("url")
+ .desc("url of zip file")
+ .hasArg().build());
+ options.addOption(Option.builder("c").longOpt("collection")
+ .desc("destination collection(s) Handle or database ID")
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("m").longOpt("mapfile")
+ .desc("mapfile items in mapfile")
+ .type(InputStream.class)
+ .hasArg().required(false).build());
+ options.addOption(Option.builder("w").longOpt("workflow")
+ .desc("send submission through collection's workflow")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("n").longOpt("notify")
+ .desc("if sending submissions through the workflow, send notification emails")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("v").longOpt("validate")
+ .desc("test run - do not actually import items")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("x").longOpt("exclude-bitstreams")
+ .desc("do not load or expect content bitstreams")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("p").longOpt("template")
+ .desc("apply template")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("R").longOpt("resume")
+ .desc("resume a failed import (add only)")
+ .hasArg(false).required(false).build());
+ options.addOption(Option.builder("q").longOpt("quiet")
+ .desc("don't display metadata")
+ .hasArg(false).required(false).build());
+
+ options.addOption(Option.builder("h").longOpt("help")
+ .desc("help")
+ .hasArg(false).required(false).build());
+
+ return options;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
index 6a6a70d574dc..5eaeb326ffc4 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java
@@ -7,6 +7,7 @@
*/
package org.dspace.app.itemimport;
+import static org.dspace.core.Constants.CONTENT_BUNDLE_NAME;
import static org.dspace.iiif.util.IIIFSharedUtils.METADATA_IIIF_HEIGHT_QUALIFIER;
import static org.dspace.iiif.util.IIIFSharedUtils.METADATA_IIIF_IMAGE_ELEMENT;
import static org.dspace.iiif.util.IIIFSharedUtils.METADATA_IIIF_LABEL_ELEMENT;
@@ -41,6 +42,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.UUID;
@@ -51,15 +53,20 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ComparatorUtils;
import org.apache.commons.io.FileDeleteStrategy;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.apache.xpath.XPathAPI;
import org.dspace.app.itemimport.service.ItemImportService;
import org.dspace.app.util.LocalSchemaFilenameFilter;
import org.dspace.app.util.RelationshipUtils;
@@ -80,6 +87,7 @@
import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType;
import org.dspace.content.WorkspaceItem;
+import org.dspace.content.clarin.ClarinLicense;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.BundleService;
@@ -92,6 +100,9 @@
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.content.service.WorkspaceItemService;
+import org.dspace.content.service.clarin.ClarinItemService;
+import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService;
+import org.dspace.content.service.clarin.ClarinLicenseService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.Email;
@@ -102,6 +113,7 @@
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.handle.service.HandleService;
+import org.dspace.scripts.handler.DSpaceRunnableHandler;
import org.dspace.services.ConfigurationService;
import org.dspace.workflow.WorkflowItem;
import org.dspace.workflow.WorkflowService;
@@ -131,7 +143,9 @@
* allow the registration of files (bitstreams) into DSpace.
*/
public class ItemImportServiceImpl implements ItemImportService, InitializingBean {
- private final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemImportServiceImpl.class);
+ private final Logger log = LogManager.getLogger();
+
+ private DSpaceRunnableHandler handler;
@Autowired(required = true)
protected AuthorizeService authorizeService;
@@ -171,10 +185,17 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea
protected RelationshipTypeService relationshipTypeService;
@Autowired(required = true)
protected MetadataValueService metadataValueService;
+ @Autowired(required = true)
+ protected ClarinLicenseService clarinLicenseService;
+ @Autowired(required = true)
+ protected ClarinLicenseResourceMappingService clarinLicenseResourceMappingService;
+ @Autowired(required = true)
+ protected ClarinItemService clarinItemService;
protected String tempWorkDir;
protected boolean isTest = false;
+ protected boolean isExcludeContent = false;
protected boolean isResume = false;
protected boolean useWorkflow = false;
protected boolean useWorkflowSendEmail = false;
@@ -191,11 +212,13 @@ public void afterPropertiesSet() throws Exception {
if (!tempWorkDirFile.exists()) {
boolean success = tempWorkDirFile.mkdir();
if (success) {
- log.info("Created org.dspace.app.batchitemimport.work.dir of: " + tempWorkDir);
+ logInfo("Created org.dspace.app.batchitemimport.work.dir of: " + tempWorkDir);
} else {
- log.error("Cannot create batch import directory! " + tempWorkDir);
+ logError("Cannot create batch import directory! " + tempWorkDir);
}
}
+ // clean work dir path from duplicate separators
+ tempWorkDir = StringUtils.replace(tempWorkDir, File.separator + File.separator, File.separator);
}
// File listing filter to look for metadata files
@@ -221,9 +244,9 @@ public void addItemsAtomic(Context c, List mycollections, String sou
try {
addItems(c, mycollections, sourceDir, mapFile, template);
} catch (Exception addException) {
- log.error("AddItems encountered an error, will try to revert. Error: " + addException.getMessage());
+ logError("AddItems encountered an error, will try to revert. Error: " + addException.getMessage());
deleteItems(c, mapFile);
- log.info("Attempted to delete partial (errored) import");
+ logInfo("Attempted to delete partial (errored) import");
throw addException;
}
}
@@ -241,10 +264,8 @@ public void addItems(Context c, List mycollections,
itemFolderMap = new HashMap<>();
- System.out.println("Adding items from directory: " + sourceDir);
- log.debug("Adding items from directory: " + sourceDir);
- System.out.println("Generating mapfile: " + mapFile);
- log.debug("Generating mapfile: " + mapFile);
+ logDebug("Adding items from directory: " + sourceDir);
+ logDebug("Generating mapfile: " + mapFile);
boolean directoryFileCollections = false;
if (mycollections == null) {
@@ -261,16 +282,12 @@ public void addItems(Context c, List mycollections,
// sneaky isResume == true means open file in append mode
outFile = new File(mapFile);
mapOut = new PrintWriter(new FileWriter(outFile, isResume));
-
- if (mapOut == null) {
- throw new Exception("can't open mapfile: " + mapFile);
- }
}
// open and process the source directory
File d = new java.io.File(sourceDir);
- if (d == null || !d.isDirectory()) {
+ if (!d.isDirectory()) {
throw new Exception("Error, cannot open source directory " + sourceDir);
}
@@ -280,7 +297,7 @@ public void addItems(Context c, List mycollections,
for (int i = 0; i < dircontents.length; i++) {
if (skipItems.containsKey(dircontents[i])) {
- System.out.println("Skipping import of " + dircontents[i]);
+ logInfo("Skipping import of " + dircontents[i]);
//we still need the item in the map for relationship linking
String skippedHandle = skipItems.get(dircontents[i]);
@@ -294,13 +311,12 @@ public void addItems(Context c, List mycollections,
try {
List cols = processCollectionFile(c, path, "collections");
if (cols == null) {
- System.out
- .println("No collections specified for item " + dircontents[i] + ". Skipping.");
+ logError("No collections specified for item " + dircontents[i] + ". Skipping.");
continue;
}
clist = cols;
} catch (IllegalArgumentException e) {
- System.out.println(e.getMessage() + " Skipping.");
+ logError(e.getMessage() + " Skipping.");
continue;
}
} else {
@@ -312,7 +328,7 @@ public void addItems(Context c, List mycollections,
itemFolderMap.put(dircontents[i], item);
c.uncacheEntity(item);
- System.out.println(i + " " + dircontents[i]);
+ logInfo(i + " " + dircontents[i]);
}
}
@@ -354,7 +370,7 @@ protected void addRelationships(Context c, String sourceDir) throws Exception {
for (String itemIdentifier : identifierList) {
if (isTest) {
- System.out.println("\tAdding relationship (type: " + relationshipType +
+ logInfo("\tAdding relationship (type: " + relationshipType +
") from " + folderName + " to " + itemIdentifier);
continue;
}
@@ -365,58 +381,70 @@ protected void addRelationships(Context c, String sourceDir) throws Exception {
throw new Exception("Could not find item for " + itemIdentifier);
}
- //get entity type of entity and item
- String itemEntityType = getEntityType(item);
- String relatedEntityType = getEntityType(relationItem);
-
- //find matching relationship type
- List relTypes = relationshipTypeService.findByLeftwardOrRightwardTypeName(
- c, relationshipType);
- RelationshipType foundRelationshipType = RelationshipUtils.matchRelationshipType(
- relTypes, relatedEntityType, itemEntityType, relationshipType);
-
- if (foundRelationshipType == null) {
- throw new Exception("No Relationship type found for:\n" +
- "Target type: " + relatedEntityType + "\n" +
- "Origin referer type: " + itemEntityType + "\n" +
- "with typeName: " + relationshipType
- );
- }
-
- boolean left = false;
- if (foundRelationshipType.getLeftwardType().equalsIgnoreCase(relationshipType)) {
- left = true;
- }
+ addRelationship(c, item, relationItem, relationshipType);
+ }
- // Placeholder items for relation placing
- Item leftItem = null;
- Item rightItem = null;
- if (left) {
- leftItem = item;
- rightItem = relationItem;
- } else {
- leftItem = relationItem;
- rightItem = item;
- }
+ }
- // Create the relationship
- int leftPlace = relationshipService.findNextLeftPlaceByLeftItem(c, leftItem);
- int rightPlace = relationshipService.findNextRightPlaceByRightItem(c, rightItem);
- Relationship persistedRelationship = relationshipService.create(
- c, leftItem, rightItem, foundRelationshipType, leftPlace, rightPlace);
- // relationshipService.update(c, persistedRelationship);
+ }
- System.out.println("\tAdded relationship (type: " + relationshipType + ") from " +
- leftItem.getHandle() + " to " + rightItem.getHandle());
+ }
- }
+ }
- }
+ /**
+ * Add relationship.
+ * @param c the context
+ * @param item the item
+ * @param relationItem the related item
+ * @param relationshipType the relation type name
+ * @throws SQLException
+ * @throws AuthorizeException
+ */
+ protected void addRelationship(Context c, Item item, Item relationItem, String relationshipType)
+ throws SQLException, AuthorizeException {
+ // get entity type of entity and item
+ String itemEntityType = getEntityType(item);
+ String relatedEntityType = getEntityType(relationItem);
+
+ // find matching relationship type
+ List relTypes = relationshipTypeService.findByLeftwardOrRightwardTypeName(
+ c, relationshipType);
+ RelationshipType foundRelationshipType = RelationshipUtils.matchRelationshipType(
+ relTypes, relatedEntityType, itemEntityType, relationshipType);
+
+ if (foundRelationshipType == null) {
+ throw new IllegalArgumentException("No Relationship type found for:\n" +
+ "Target type: " + relatedEntityType + "\n" +
+ "Origin referer type: " + itemEntityType + "\n" +
+ "with typeName: " + relationshipType
+ );
+ }
- }
+ boolean left = false;
+ if (foundRelationshipType.getLeftwardType().equalsIgnoreCase(relationshipType)) {
+ left = true;
+ }
+ // placeholder items for relation placing
+ Item leftItem = null;
+ Item rightItem = null;
+ if (left) {
+ leftItem = item;
+ rightItem = relationItem;
+ } else {
+ leftItem = relationItem;
+ rightItem = item;
}
+ // Create the relationship, appending to the end
+ Relationship persistedRelationship = relationshipService.create(
+ c, leftItem, rightItem, foundRelationshipType, -1, -1
+ );
+ relationshipService.update(c, persistedRelationship);
+
+ logInfo("\tAdded relationship (type: " + relationshipType + ") from " +
+ leftItem.getHandle() + " to " + rightItem.getHandle());
}
/**
@@ -425,19 +453,23 @@ protected void addRelationships(Context c, String sourceDir) throws Exception {
* @param item
* @return
*/
- protected String getEntityType(Item item) throws Exception {
+ protected String getEntityType(Item item) {
return itemService.getMetadata(item, "dspace", "entity", "type", Item.ANY).get(0).getValue();
}
/**
* Read the relationship manifest file.
*
- * Each line in the file contains a relationship type id and an item identifier in the following format:
- *
- * relation.
- *
- * The input_item_folder should refer the folder name of another item in this import batch.
- *
+ * Each line in the file contains a relationship type id and an item
+ * identifier in the following format:
+ *
+ *
+ * {@code relation. }
+ *
+ *
+ * The {@code input_item_folder} should refer the folder name of another
+ * item in this import batch.
+ *
* @param path The main import folder path.
* @param filename The name of the manifest file to check ('relationships')
* @return Map of found relationships
@@ -450,7 +482,7 @@ protected Map> processRelationshipFile(String path, String
if (file.exists()) {
- System.out.println("\tProcessing relationships file: " + filename);
+ logInfo("\tProcessing relationships file: " + filename);
BufferedReader br = null;
try {
@@ -491,13 +523,13 @@ protected Map> processRelationshipFile(String path, String
}
} catch (FileNotFoundException e) {
- System.out.println("\tNo relationships file found.");
+ logWarn("\tNo relationships file found.");
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
- System.out.println("Non-critical problem releasing resources.");
+ logError("Non-critical problem releasing resources.");
}
}
}
@@ -541,25 +573,41 @@ protected Item resolveRelatedItem(Context c, String itemIdentifier) throws Excep
}
- } else if (itemIdentifier.indexOf('/') != -1) {
- //resolve by handle
- return (Item) handleService.resolveToObject(c, itemIdentifier);
-
- } else {
- //try to resolve by UUID
- return itemService.findByIdOrLegacyId(c, itemIdentifier);
}
- return null;
+ // resolve item by handle or UUID
+ return resolveItem(c, itemIdentifier);
}
+ /**
+ * Resolve an item identifier.
+ *
+ * @param c Context
+ * @param itemIdentifier The identifier string found in the import file (handle or UUID)
+ * @return Item if found, or null.
+ * @throws SQLException
+ * @throws IllegalStateException
+ * @throws Exception
+ */
+ protected Item resolveItem(Context c, String itemIdentifier)
+ throws IllegalStateException, SQLException {
+ if (itemIdentifier.indexOf('/') != -1) {
+ // resolve by handle
+ return (Item) handleService.resolveToObject(c, itemIdentifier);
+ }
+
+ // resolve by UUID
+ return itemService.findByIdOrLegacyId(c, itemIdentifier);
+ }
+
/**
* Lookup an item by a (unique) meta value.
*
- * @param metaKey
- * @param metaValue
- * @return Item
+ * @param c current DSpace session.
+ * @param metaKey name of the metadata field to match.
+ * @param metaValue value to be matched.
+ * @return the matching Item.
* @throws Exception if single item not found.
*/
protected Item findItemByMetaValue(Context c, String metaKey, String metaValue) throws Exception {
@@ -603,7 +651,7 @@ public void replaceItems(Context c, List mycollections,
// verify the source directory
File d = new java.io.File(sourceDir);
- if (d == null || !d.isDirectory()) {
+ if (!d.isDirectory()) {
throw new Exception("Error, cannot open source directory "
+ sourceDir);
}
@@ -621,7 +669,7 @@ public void replaceItems(Context c, List mycollections,
Item oldItem = null;
if (oldHandle.indexOf('/') != -1) {
- System.out.println("\tReplacing: " + oldHandle);
+ logInfo("\tReplacing: " + oldHandle);
// add new item, locate old one
oldItem = (Item) handleService.resolveToObject(c, oldHandle);
@@ -642,10 +690,6 @@ public void replaceItems(Context c, List mycollections,
File handleFile = new File(sourceDir + File.separatorChar + newItemName + File.separatorChar + "handle");
PrintWriter handleOut = new PrintWriter(new FileWriter(handleFile, true));
- if (handleOut == null) {
- throw new Exception("can't open handle file: " + handleFile.getCanonicalPath());
- }
-
handleOut.println(oldHandle);
handleOut.close();
@@ -653,12 +697,45 @@ public void replaceItems(Context c, List mycollections,
Item newItem = addItem(c, mycollections, sourceDir, newItemName, null, template);
c.uncacheEntity(oldItem);
c.uncacheEntity(newItem);
+
+ // attach license, license label requires an update
+ // get license name and check if exists and is not null, license name is stored in the metadatum
+ // `dc.rights`
+ List dcRights =
+ itemService.getMetadata(newItem, "dc", "rights", null, Item.ANY);
+ if (CollectionUtils.isEmpty(dcRights) || Objects.isNull(dcRights.get(0))) {
+ log.error("Item doesn't have the Clarin License name in the metadata `dc.rights`.");
+ continue;
+ }
+
+ final String licenseName = dcRights.get(0).getValue();
+ if (Objects.isNull(licenseName)) {
+ log.error("License name loaded from the `dc.rights` is null.");
+ continue;
+ }
+
+ final ClarinLicense license = clarinLicenseService.findByName(c, licenseName);
+ for (Bundle bundle : newItem.getBundles(CONTENT_BUNDLE_NAME)) {
+ for (Bitstream b : bundle.getBitstreams()) {
+ this.clarinLicenseResourceMappingService.detachLicenses(c, b);
+ // add the license to bitstream
+ this.clarinLicenseResourceMappingService.attachLicense(c, license, b);
+ }
+ }
+
+ itemService.clearMetadata(c, newItem, "dc", "rights", "label", Item.ANY);
+ itemService.addMetadata(c, newItem, "dc", "rights", "label", Item.ANY,
+ license.getNonExtendedClarinLicenseLabel().getLabel());
+ clarinItemService.updateItemFilesMetadata(c, newItem);
+
+ itemService.update(c, newItem);
+ c.uncacheEntity(newItem);
}
}
@Override
public void deleteItems(Context c, String mapFile) throws Exception {
- System.out.println("Deleting items listed in mapfile: " + mapFile);
+ logInfo("Deleting items listed in mapfile: " + mapFile);
// read in the mapfile
Map myhash = readMapFile(mapFile);
@@ -671,12 +748,12 @@ public void deleteItems(Context c, String mapFile) throws Exception {
if (itemID.indexOf('/') != -1) {
String myhandle = itemID;
- System.out.println("Deleting item " + myhandle);
+ logInfo("Deleting item " + myhandle);
deleteItem(c, myhandle);
} else {
// it's an ID
Item myitem = itemService.findByIdOrLegacyId(c, itemID);
- System.out.println("Deleting item " + itemID);
+ logInfo("Deleting item " + itemID);
deleteItem(c, myitem);
c.uncacheEntity(myitem);
}
@@ -699,8 +776,7 @@ protected Item addItem(Context c, List mycollections, String path,
String itemname, PrintWriter mapOut, boolean template) throws Exception {
String mapOutputString = null;
- System.out.println("Adding item from directory " + itemname);
- log.debug("adding item from directory " + itemname);
+ logDebug("adding item from directory " + itemname);
// create workspace item
Item myitem = null;
@@ -744,10 +820,14 @@ protected Item addItem(Context c, List mycollections, String path,
// put item in system
if (!isTest) {
try {
+ // Add provenance info
+ String provenance = installItemService.getSubmittedByProvenanceMessage(c, wi.getItem());
+ itemService.addMetadata(c, wi.getItem(), MetadataSchemaEnum.DC.getName(),
+ "description", "provenance", "en", provenance);
installItemService.installItem(c, wi, myhandle);
} catch (Exception e) {
workspaceItemService.deleteAll(c, wi);
- log.error("Exception after install item, try to revert...", e);
+ logError("Exception after install item, try to revert...", e);
throw e;
}
@@ -759,7 +839,7 @@ protected Item addItem(Context c, List mycollections, String path,
// set permissions if specified in contents file
if (options.size() > 0) {
- System.out.println("Processing options");
+ logInfo("Processing options");
processOptions(c, myitem, options);
}
}
@@ -810,7 +890,7 @@ protected void deleteItem(Context c, String myhandle) throws Exception {
Item myitem = (Item) handleService.resolveToObject(c, myhandle);
if (myitem == null) {
- System.out.println("Error - cannot locate item - already deleted?");
+ logError("Error - cannot locate item - already deleted?");
} else {
deleteItem(c, myitem);
c.uncacheEntity(myitem);
@@ -863,7 +943,7 @@ protected Map readMapFile(String filename) throws Exception {
// Load all metadata schemas into the item.
protected void loadMetadata(Context c, Item myitem, String path)
throws SQLException, IOException, ParserConfigurationException,
- SAXException, TransformerException, AuthorizeException {
+ SAXException, TransformerException, AuthorizeException, XPathExpressionException {
// Load the dublin core metadata
loadDublinCore(c, myitem, path + "dublin_core.xml");
@@ -877,14 +957,15 @@ protected void loadMetadata(Context c, Item myitem, String path)
protected void loadDublinCore(Context c, Item myitem, String filename)
throws SQLException, IOException, ParserConfigurationException,
- SAXException, TransformerException, AuthorizeException {
+ SAXException, TransformerException, AuthorizeException, XPathExpressionException {
Document document = loadXML(filename);
// Get the schema, for backward compatibility we will default to the
// dublin core schema if the schema name is not available in the import
// file
String schema;
- NodeList metadata = XPathAPI.selectNodeList(document, "/dublin_core");
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList metadata = (NodeList) xPath.compile("/dublin_core").evaluate(document, XPathConstants.NODESET);
Node schemaAttr = metadata.item(0).getAttributes().getNamedItem(
"schema");
if (schemaAttr == null) {
@@ -894,11 +975,10 @@ protected void loadDublinCore(Context c, Item myitem, String filename)
}
// Get the nodes corresponding to formats
- NodeList dcNodes = XPathAPI.selectNodeList(document,
- "/dublin_core/dcvalue");
+ NodeList dcNodes = (NodeList) xPath.compile("/dublin_core/dcvalue").evaluate(document, XPathConstants.NODESET);
if (!isQuiet) {
- System.out.println("\tLoading dublin core from " + filename);
+ logInfo("\tLoading dublin core from " + filename);
}
// Add each one as a new format to the registry
@@ -922,13 +1002,14 @@ protected void addDCValue(Context c, Item i, String schema, Node n)
String qualifier = getAttributeValue(n, "qualifier"); //NodeValue();
// //getElementData(n,
// "qualifier");
- String language = getAttributeValue(n, "language");
- if (language != null) {
- language = language.trim();
+
+ String language = null;
+ if (StringUtils.isNotBlank(getAttributeValue(n, "language"))) {
+ language = getAttributeValue(n, "language").trim();
}
if (!isQuiet) {
- System.out.println("\tSchema: " + schema + " Element: " + element + " Qualifier: " + qualifier
+ logInfo("\tSchema: " + schema + " Element: " + element + " Qualifier: " + qualifier
+ " Value: " + value);
}
@@ -937,20 +1018,28 @@ protected void addDCValue(Context c, Item i, String schema, Node n)
}
// only add metadata if it is no test and there is an actual value
if (!isTest && !value.equals("")) {
- itemService.addMetadata(c, i, schema, element, qualifier, language, value);
+ if (StringUtils.equals(schema, MetadataSchemaEnum.RELATION.getName())) {
+ Item relationItem = resolveItem(c, value);
+ if (relationItem == null) {
+ throw new IllegalArgumentException("No item found with id=" + value);
+ }
+ addRelationship(c, i, relationItem, element);
+ } else {
+ itemService.addMetadata(c, i, schema, element, qualifier, language, value);
+ }
} else {
// If we're just test the import, let's check that the actual metadata field exists.
MetadataSchema foundSchema = metadataSchemaService.find(c, schema);
if (foundSchema == null) {
- System.out.println("ERROR: schema '" + schema + "' was not found in the registry.");
+ logError("ERROR: schema '" + schema + "' was not found in the registry.");
return;
}
MetadataField foundField = metadataFieldService.findByElement(c, foundSchema, element, qualifier);
if (foundField == null) {
- System.out.println(
+ logError(
"ERROR: Metadata field: '" + schema + "." + element + "." + qualifier + "' was not found in the " +
"registry.");
return;
@@ -977,7 +1066,7 @@ protected List processCollectionFile(Context c, String path, String
File file = new File(path + File.separatorChar + filename);
ArrayList collections = new ArrayList<>();
List result = null;
- System.out.println("Processing collections file: " + filename);
+ logInfo("Processing collections file: " + filename);
if (file.exists()) {
BufferedReader br = null;
@@ -1004,13 +1093,13 @@ protected List processCollectionFile(Context c, String path, String
result = collections;
} catch (FileNotFoundException e) {
- System.out.println("No collections file found.");
+ logWarn("No collections file found.");
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
- System.out.println("Non-critical problem releasing resources.");
+ logError("Non-critical problem releasing resources.");
}
}
}
@@ -1032,7 +1121,7 @@ protected String processHandleFile(Context c, Item i, String path, String filena
File file = new File(path + File.separatorChar + filename);
String result = null;
- System.out.println("Processing handle file: " + filename);
+ logInfo("Processing handle file: " + filename);
if (file.exists()) {
BufferedReader is = null;
try {
@@ -1041,14 +1130,14 @@ protected String processHandleFile(Context c, Item i, String path, String filena
// result gets contents of file, or null
result = is.readLine();
- System.out.println("read handle: '" + result + "'");
+ logInfo("read handle: '" + result + "'");
} catch (FileNotFoundException e) {
// probably no handle file, just return null
- System.out.println("It appears there is no handle file -- generating one");
+ logWarn("It appears there is no handle file -- generating one");
} catch (IOException e) {
// probably no handle file, just return null
- System.out.println("It appears there is no handle file -- generating one");
+ logWarn("It appears there is no handle file -- generating one");
} finally {
if (is != null) {
try {
@@ -1060,7 +1149,7 @@ protected String processHandleFile(Context c, Item i, String path, String filena
}
} else {
// probably no handle file, just return null
- System.out.println("It appears there is no handle file -- generating one");
+ logWarn("It appears there is no handle file -- generating one");
}
return result;
@@ -1087,7 +1176,7 @@ protected List processContentsFile(Context c, Item i, String path,
String line = "";
List options = new ArrayList<>();
- System.out.println("\tProcessing contents file: " + contentsFile);
+ logInfo("\tProcessing contents file: " + contentsFile);
if (contentsFile.exists()) {
BufferedReader is = null;
@@ -1134,8 +1223,8 @@ protected List processContentsFile(Context c, Item i, String path,
}
} // while
if (iAssetstore == -1 || sFilePath == null) {
- System.out.println("\tERROR: invalid contents file line");
- System.out.println("\t\tSkipping line: "
+ logError("\tERROR: invalid contents file line");
+ logInfo("\t\tSkipping line: "
+ sRegistrationLine);
continue;
}
@@ -1159,7 +1248,7 @@ protected List processContentsFile(Context c, Item i, String path,
}
registerBitstream(c, i, iAssetstore, sFilePath, sBundle, sDescription);
- System.out.println("\tRegistering Bitstream: " + sFilePath
+ logInfo("\tRegistering Bitstream: " + sFilePath
+ "\tAssetstore: " + iAssetstore
+ "\tBundle: " + sBundle
+ "\tDescription: " + sDescription);
@@ -1171,7 +1260,7 @@ protected List processContentsFile(Context c, Item i, String path,
if (bitstreamEndIndex == -1) {
// no extra info
processContentFileEntry(c, i, path, line, null, false);
- System.out.println("\tBitstream: " + line);
+ logInfo("\tBitstream: " + line);
} else {
String bitstreamName = line.substring(0, bitstreamEndIndex);
@@ -1283,17 +1372,17 @@ protected List processContentsFile(Context c, Item i, String path,
+ bundleMarker.length(), bEndIndex).trim();
processContentFileEntry(c, i, path, bitstreamName, bundleName, primary);
- System.out.println("\tBitstream: " + bitstreamName +
+ logInfo("\tBitstream: " + bitstreamName +
"\tBundle: " + bundleName +
primaryStr);
} else {
processContentFileEntry(c, i, path, bitstreamName, null, primary);
- System.out.println("\tBitstream: " + bitstreamName + primaryStr);
+ logInfo("\tBitstream: " + bitstreamName + primaryStr);
}
if (permissionsExist || descriptionExists || labelExists || heightExists
|| widthExists || tocExists) {
- System.out.println("Gathering options.");
+ logInfo("Gathering options.");
String extraInfo = bitstreamName;
if (permissionsExist) {
@@ -1340,12 +1429,12 @@ protected List processContentsFile(Context c, Item i, String path,
String[] dirListing = dir.list();
for (String fileName : dirListing) {
if (!"dublin_core.xml".equals(fileName) && !fileName.equals("handle") && !metadataFileFilter
- .accept(dir, fileName)) {
+ .accept(dir, fileName) && !"collections".equals(fileName) && !"relationships".equals(fileName)) {
throw new FileNotFoundException("No contents file found");
}
}
- System.out.println("No contents file found - but only metadata files found. Assuming metadata only.");
+ logInfo("No contents file found - but only metadata files found. Assuming metadata only.");
}
return options;
@@ -1367,6 +1456,10 @@ protected List processContentsFile(Context c, Item i, String path,
protected void processContentFileEntry(Context c, Item i, String path,
String fileName, String bundleName, boolean primary) throws SQLException,
IOException, AuthorizeException {
+ if (isExcludeContent) {
+ return;
+ }
+
String fullpath = path + File.separatorChar + fileName;
// get an input stream
@@ -1507,9 +1600,9 @@ protected void registerBitstream(Context c, Item i, int assetstore,
*/
protected void processOptions(Context c, Item myItem, List options)
throws SQLException, AuthorizeException {
- System.out.println("Processing options.");
+ logInfo("Processing options.");
for (String line : options) {
- System.out.println("\tprocessing " + line);
+ logInfo("\tprocessing " + line);
boolean permissionsExist = false;
boolean descriptionExists = false;
@@ -1626,7 +1719,7 @@ protected void processOptions(Context c, Item myItem, List options)
try {
myGroup = groupService.findByName(c, groupName);
} catch (SQLException sqle) {
- System.out.println("SQL Exception finding group name: "
+ logError("SQL Exception finding group name: "
+ groupName);
// do nothing, will check for null group later
}
@@ -1667,42 +1760,41 @@ protected void processOptions(Context c, Item myItem, List options)
.trim();
}
+ if (isTest) {
+ continue;
+ }
+
Bitstream bs = null;
- boolean notfound = true;
boolean updateRequired = false;
- if (!isTest) {
- // find bitstream
- List bitstreams = itemService.getNonInternalBitstreams(c, myItem);
- for (int j = 0; j < bitstreams.size() && notfound; j++) {
- if (bitstreams.get(j).getName().equals(bitstreamName)) {
- bs = bitstreams.get(j);
- notfound = false;
- }
+ // find bitstream
+ List bitstreams = itemService.getNonInternalBitstreams(c, myItem);
+ for (Bitstream bitstream : bitstreams) {
+ if (bitstream.getName().equals(bitstreamName)) {
+ bs = bitstream;
+ break;
}
}
- if (notfound && !isTest) {
+ if (null == bs) {
// this should never happen
- System.out.println("\tdefault permissions set for "
- + bitstreamName);
- } else if (!isTest) {
+ logInfo("\tdefault permissions set for " + bitstreamName);
+ } else {
if (permissionsExist) {
if (myGroup == null) {
- System.out.println("\t" + groupName
+ logInfo("\t" + groupName
+ " not found, permissions set to default");
} else if (actionID == -1) {
- System.out
- .println("\tinvalid permissions flag, permissions set to default");
+ logInfo("\tinvalid permissions flag, permissions set to default");
} else {
- System.out.println("\tSetting special permissions for "
+ logInfo("\tSetting special permissions for "
+ bitstreamName);
setPermission(c, myGroup, actionID, bs);
}
}
if (descriptionExists) {
- System.out.println("\tSetting description for "
+ logInfo("\tSetting description for "
+ bitstreamName);
bs.setDescription(c, thisDescription);
updateRequired = true;
@@ -1711,7 +1803,7 @@ protected void processOptions(Context c, Item myItem, List options)
if (labelExists) {
MetadataField metadataField = metadataFieldService
.findByElement(c, METADATA_IIIF_SCHEMA, METADATA_IIIF_LABEL_ELEMENT, null);
- System.out.println("\tSetting label to " + thisLabel + " in element "
+ logInfo("\tSetting label to " + thisLabel + " in element "
+ metadataField.getElement() + " on " + bitstreamName);
bitstreamService.addMetadata(c, bs, metadataField, null, thisLabel);
updateRequired = true;
@@ -1721,7 +1813,7 @@ protected void processOptions(Context c, Item myItem, List options)
MetadataField metadataField = metadataFieldService
.findByElement(c, METADATA_IIIF_SCHEMA, METADATA_IIIF_IMAGE_ELEMENT,
METADATA_IIIF_HEIGHT_QUALIFIER);
- System.out.println("\tSetting height to " + thisHeight + " in element "
+ logInfo("\tSetting height to " + thisHeight + " in element "
+ metadataField.getElement() + " on " + bitstreamName);
bitstreamService.addMetadata(c, bs, metadataField, null, thisHeight);
updateRequired = true;
@@ -1730,7 +1822,7 @@ protected void processOptions(Context c, Item myItem, List options)
MetadataField metadataField = metadataFieldService
.findByElement(c, METADATA_IIIF_SCHEMA, METADATA_IIIF_IMAGE_ELEMENT,
METADATA_IIIF_WIDTH_QUALIFIER);
- System.out.println("\tSetting width to " + thisWidth + " in element "
+ logInfo("\tSetting width to " + thisWidth + " in element "
+ metadataField.getElement() + " on " + bitstreamName);
bitstreamService.addMetadata(c, bs, metadataField, null, thisWidth);
updateRequired = true;
@@ -1738,7 +1830,7 @@ protected void processOptions(Context c, Item myItem, List options)
if (tocExists) {
MetadataField metadataField = metadataFieldService
.findByElement(c, METADATA_IIIF_SCHEMA, METADATA_IIIF_TOC_ELEMENT, null);
- System.out.println("\tSetting toc to " + thisToc + " in element "
+ logInfo("\tSetting toc to " + thisToc + " in element "
+ metadataField.getElement() + " on " + bitstreamName);
bitstreamService.addMetadata(c, bs, metadataField, null, thisToc);
updateRequired = true;
@@ -1777,9 +1869,9 @@ protected void setPermission(Context c, Group g, int actionID, Bitstream bs)
resourcePolicyService.update(c, rp);
} else {
if (actionID == Constants.READ) {
- System.out.println("\t\tpermissions: READ for " + g.getName());
+ logInfo("\t\tpermissions: READ for " + g.getName());
} else if (actionID == Constants.WRITE) {
- System.out.println("\t\tpermissions: WRITE for " + g.getName());
+ logInfo("\t\tpermissions: WRITE for " + g.getName());
}
}
@@ -1860,7 +1952,7 @@ protected boolean deleteDirectory(File path) {
deleteDirectory(files[i]);
} else {
if (!files[i].delete()) {
- log.error("Unable to delete file: " + files[i].getName());
+ logError("Unable to delete file: " + files[i].getName());
}
}
}
@@ -1880,7 +1972,7 @@ public String unzip(File zipfile, String destDir) throws IOException {
// 2
// does the zip file exist and can we write to the temp directory
if (!zipfile.canRead()) {
- log.error("Zip file '" + zipfile.getAbsolutePath() + "' does not exist, or is not readable.");
+ logError("Zip file '" + zipfile.getAbsolutePath() + "' does not exist, or is not readable.");
}
String destinationDir = destDir;
@@ -1890,13 +1982,13 @@ public String unzip(File zipfile, String destDir) throws IOException {
File tempdir = new File(destinationDir);
if (!tempdir.isDirectory()) {
- log.error("'" + configurationService.getProperty("org.dspace.app.itemexport.work.dir") +
- "' as defined by the key 'org.dspace.app.itemexport.work.dir' in dspace.cfg " +
+ logError("'" + configurationService.getProperty("org.dspace.app.batchitemimport.work.dir") +
+ "' as defined by the key 'org.dspace.app.batchitemimport.work.dir' in dspace.cfg " +
"is not a valid directory");
}
if (!tempdir.exists() && !tempdir.mkdirs()) {
- log.error("Unable to create temporary directory: " + tempdir.getAbsolutePath());
+ logError("Unable to create temporary directory: " + tempdir.getAbsolutePath());
}
String sourcedir = destinationDir + System.getProperty("file.separator") + zipfile.getName();
String zipDir = destinationDir + System.getProperty("file.separator") + zipfile.getName() + System
@@ -1908,71 +2000,71 @@ public String unzip(File zipfile, String destDir) throws IOException {
ZipFile zf = new ZipFile(zipfile);
ZipEntry entry;
Enumeration extends ZipEntry> entries = zf.entries();
- while (entries.hasMoreElements()) {
- entry = entries.nextElement();
- if (entry.isDirectory()) {
- if (!new File(zipDir + entry.getName()).mkdirs()) {
- log.error("Unable to create contents directory: " + zipDir + entry.getName());
- }
- } else {
- String entryName = entry.getName();
- File outFile = new File(zipDir + entryName);
- // Verify that this file will be extracted into our zipDir (and not somewhere else!)
- if (!outFile.toPath().normalize().startsWith(zipDir)) {
- throw new IOException("Bad zip entry: '" + entryName
- + "' in file '" + zipfile.getAbsolutePath() + "'!"
- + " Cannot process this file.");
+ try {
+ while (entries.hasMoreElements()) {
+ entry = entries.nextElement();
+ if (entry.isDirectory()) {
+ if (!new File(zipDir + entry.getName()).mkdirs()) {
+ logError("Unable to create contents directory: " + zipDir + entry.getName());
+ }
} else {
- System.out.println("Extracting file: " + entryName);
- log.info("Extracting file: " + entryName);
+ String entryName = entry.getName();
+ File outFile = new File(zipDir + entryName);
+ // Verify that this file will be extracted into our zipDir (and not somewhere else!)
+ if (!outFile.toPath().normalize().startsWith(zipDir)) {
+ throw new IOException("Bad zip entry: '" + entryName
+ + "' in file '" + zipfile.getAbsolutePath() + "'!"
+ + " Cannot process this file.");
+ } else {
+ logInfo("Extracting file: " + entryName);
- int index = entryName.lastIndexOf('/');
- if (index == -1) {
- // Was it created on Windows instead?
- index = entryName.lastIndexOf('\\');
- }
- if (index > 0) {
- File dir = new File(zipDir + entryName.substring(0, index));
- if (!dir.exists() && !dir.mkdirs()) {
- log.error("Unable to create directory: " + dir.getAbsolutePath());
+ int index = entryName.lastIndexOf('/');
+ if (index == -1) {
+ // Was it created on Windows instead?
+ index = entryName.lastIndexOf('\\');
}
+ if (index > 0) {
+ File dir = new File(zipDir + entryName.substring(0, index));
+ if (!dir.exists() && !dir.mkdirs()) {
+ logError("Unable to create directory: " + dir.getAbsolutePath());
+ }
- //Entries could have too many directories, and we need to adjust the sourcedir
- // file1.zip (SimpleArchiveFormat / item1 / contents|dublin_core|...
- // SimpleArchiveFormat / item2 / contents|dublin_core|...
- // or
- // file2.zip (item1 / contents|dublin_core|...
- // item2 / contents|dublin_core|...
-
- //regex supports either windows or *nix file paths
- String[] entryChunks = entryName.split("/|\\\\");
- if (entryChunks.length > 2) {
- if (StringUtils.equals(sourceDirForZip, sourcedir)) {
- sourceDirForZip = sourcedir + "/" + entryChunks[0];
+ //Entries could have too many directories, and we need to adjust the sourcedir
+ // file1.zip (SimpleArchiveFormat / item1 / contents|dublin_core|...
+ // SimpleArchiveFormat / item2 / contents|dublin_core|...
+ // or
+ // file2.zip (item1 / contents|dublin_core|...
+ // item2 / contents|dublin_core|...
+
+ //regex supports either windows or *nix file paths
+ String[] entryChunks = entryName.split("/|\\\\");
+ if (entryChunks.length > 2) {
+ if (StringUtils.equals(sourceDirForZip, sourcedir)) {
+ sourceDirForZip = sourcedir + "/" + entryChunks[0];
+ }
}
}
+ byte[] buffer = new byte[1024];
+ int len;
+ InputStream in = zf.getInputStream(entry);
+ BufferedOutputStream out = new BufferedOutputStream(
+ new FileOutputStream(outFile));
+ while ((len = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, len);
+ }
+ in.close();
+ out.close();
}
- byte[] buffer = new byte[1024];
- int len;
- InputStream in = zf.getInputStream(entry);
- BufferedOutputStream out = new BufferedOutputStream(
- new FileOutputStream(outFile));
- while ((len = in.read(buffer)) >= 0) {
- out.write(buffer, 0, len);
- }
- in.close();
- out.close();
}
}
+ } finally {
+ //Close zip file
+ zf.close();
}
- //Close zip file
- zf.close();
-
if (!StringUtils.equals(sourceDirForZip, sourcedir)) {
sourcedir = sourceDirForZip;
- System.out.println("Set sourceDir using path inside of Zip: " + sourcedir);
- log.info("Set sourceDir using path inside of Zip: " + sourcedir);
+ logInfo("Set sourceDir using path inside of Zip: " + sourcedir);
}
return sourcedir;
@@ -2022,20 +2114,15 @@ public void processUIImport(String filepath, Collection owningCollection, String
final String theFilePath = filepath;
final String theInputType = inputType;
final String theResumeDir = resumeDir;
- final boolean useTemplateItem = template;
Thread go = new Thread() {
@Override
public void run() {
- Context context = null;
-
+ Context context = new Context();
String importDir = null;
EPerson eperson = null;
try {
-
- // create a new dspace context
- context = new Context();
eperson = ePersonService.find(context, oldEPerson.getID());
context.setCurrentUser(eperson);
context.turnOffAuthorisationSystem();
@@ -2046,7 +2133,8 @@ public void run() {
if (theOtherCollections != null) {
for (String colID : theOtherCollections) {
UUID colId = UUID.fromString(colID);
- if (!theOwningCollection.getID().equals(colId)) {
+ if (theOwningCollection != null
+ && !theOwningCollection.getID().equals(colId)) {
Collection col = collectionService.find(context, colId);
if (col != null) {
collectionList.add(col);
@@ -2065,7 +2153,7 @@ public void run() {
if (!importDirFile.exists()) {
boolean success = importDirFile.mkdirs();
if (!success) {
- log.info("Cannot create batch import directory!");
+ logInfo("Cannot create batch import directory!");
throw new Exception("Cannot create batch import directory!");
}
}
@@ -2197,14 +2285,14 @@ public void emailSuccessMessage(Context context, EPerson eperson,
email.send();
} catch (Exception e) {
- log.warn(LogHelper.getHeader(context, "emailSuccessMessage", "cannot notify user of import"), e);
+ logError(LogHelper.getHeader(context, "emailSuccessMessage", "cannot notify user of import"), e);
}
}
@Override
public void emailErrorMessage(EPerson eperson, String error)
throws MessagingException {
- log.warn("An error occurred during item import, the user will be notified. " + error);
+ logError("An error occurred during item import, the user will be notified. " + error);
try {
Locale supportedLocale = I18nUtil.getEPersonLocale(eperson);
Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "bte_batch_import_error"));
@@ -2214,7 +2302,7 @@ public void emailErrorMessage(EPerson eperson, String error)
email.send();
} catch (Exception e) {
- log.warn("error during item import error notification", e);
+ logError("error during item import error notification", e);
}
}
@@ -2292,18 +2380,17 @@ public File getTempWorkDirFile()
+ tempDirFile.getAbsolutePath()
+ " could not be created.");
} else {
- log.debug("Created directory " + tempDirFile.getAbsolutePath());
+ logDebug("Created directory " + tempDirFile.getAbsolutePath());
}
} else {
- log.debug("Work directory exists: " + tempDirFile.getAbsolutePath());
+ logDebug("Work directory exists: " + tempDirFile.getAbsolutePath());
}
return tempDirFile;
}
@Override
public void cleanupZipTemp() {
- System.out.println("Deleting temporary zip directory: " + tempWorkDir);
- log.debug("Deleting temporary zip directory: " + tempWorkDir);
+ logDebug("Deleting temporary zip directory: " + tempWorkDir);
deleteDirectory(new File(tempWorkDir));
}
@@ -2312,6 +2399,11 @@ public void setTest(boolean isTest) {
this.isTest = isTest;
}
+ @Override
+ public void setExcludeContent(boolean isExcludeContent) {
+ this.isExcludeContent = isExcludeContent;
+ }
+
@Override
public void setResume(boolean isResume) {
this.isResume = isResume;
@@ -2332,4 +2424,81 @@ public void setQuiet(boolean isQuiet) {
this.isQuiet = isQuiet;
}
+ @Override
+ public void setHandler(DSpaceRunnableHandler handler) {
+ this.handler = handler;
+ }
+
+ private void logInfo(String message) {
+ logInfo(message, null);
+ }
+
+ private void logInfo(String message, Exception e) {
+ if (handler != null) {
+ handler.logInfo(message);
+ return;
+ }
+
+ if (e != null) {
+ log.info(message, e);
+ } else {
+ log.info(message);
+ }
+ }
+
+ private void logDebug(String message) {
+ logDebug(message, null);
+ }
+
+ private void logDebug(String message, Exception e) {
+ if (handler != null) {
+ handler.logDebug(message);
+ return;
+ }
+
+ if (e != null) {
+ log.debug(message, e);
+ } else {
+ log.debug(message);
+ }
+ }
+
+ private void logWarn(String message) {
+ logWarn(message, null);
+ }
+
+ private void logWarn(String message, Exception e) {
+ if (handler != null) {
+ handler.logWarning(message);
+ return;
+ }
+
+ if (e != null) {
+ log.warn(message, e);
+ } else {
+ log.warn(message);
+ }
+ }
+
+ private void logError(String message) {
+ logError(message, null);
+ }
+
+ private void logError(String message, Exception e) {
+ if (handler != null) {
+ if (e != null) {
+ handler.logError(message, e);
+ } else {
+ handler.logError(message);
+ }
+ return;
+ }
+
+ if (e != null) {
+ log.error(message, e);
+ } else {
+ log.error(message);
+ }
+ }
+
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java b/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
index 2d648e2416c9..e99ece31b9bb 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemimport/service/ItemImportService.java
@@ -16,6 +16,7 @@
import org.dspace.content.Collection;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
+import org.dspace.scripts.handler.DSpaceRunnableHandler;
/**
* Import items into DSpace. The conventional use is upload files by copying
@@ -210,6 +211,13 @@ public void replaceItems(Context c, List mycollections, String sourc
*/
public void setTest(boolean isTest);
+ /**
+ * Set exclude-content flag.
+ *
+ * @param isExcludeContent true or false
+ */
+ public void setExcludeContent(boolean isExcludeContent);
+
/**
* Set resume flag
*
@@ -235,4 +243,10 @@ public void replaceItems(Context c, List mycollections, String sourc
* @param isQuiet true or false
*/
public void setQuiet(boolean isQuiet);
+
+ /**
+ * Set the DSpace Runnable Handler
+ * @param handler
+ */
+ public void setHandler(DSpaceRunnableHandler handler);
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/AddBitstreamsAction.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/AddBitstreamsAction.java
index e9693fb3d1ab..644745304a23 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemupdate/AddBitstreamsAction.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/AddBitstreamsAction.java
@@ -77,7 +77,7 @@ public void execute(Context context, ItemArchive itarch, boolean isTest,
ItemUpdate.pr("Contents bitstream count: " + contents.size());
String[] files = dir.list(ItemUpdate.fileFilter);
- List fileList = new ArrayList();
+ List fileList = new ArrayList<>();
for (String filename : files) {
fileList.add(filename);
ItemUpdate.pr("file: " + filename);
@@ -134,9 +134,6 @@ protected String addBitstream(Context context, ItemArchive itarch, Item item, Fi
ItemUpdate.pr("contents entry for bitstream: " + ce.toString());
File f = new File(dir, ce.filename);
- // get an input stream
- BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
-
Bitstream bs = null;
String newBundleName = ce.bundlename;
@@ -173,7 +170,9 @@ protected String addBitstream(Context context, ItemArchive itarch, Item item, Fi
targetBundle = bundles.iterator().next();
}
- bs = bitstreamService.create(context, targetBundle, bis);
+ try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));) {
+ bs = bitstreamService.create(context, targetBundle, bis);
+ }
bs.setName(context, ce.filename);
// Identify the format
diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemUpdate.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemUpdate.java
index b6aa875f29b0..a3fe0b2321f7 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemUpdate.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemUpdate.java
@@ -39,29 +39,34 @@
import org.dspace.handle.service.HandleService;
/**
- * Provides some batch editing capabilities for items in DSpace:
- * Metadata fields - Add, Delete
- * Bitstreams - Add, Delete
+ * Provides some batch editing capabilities for items in DSpace.
+ *
+ * - Metadata fields - Add, Delete
+ * - Bitstreams - Add, Delete
+ *
*
- * The design has been for compatibility with ItemImporter
+ *
+ * The design has been for compatibility with
+ * {@link org.dspace.app.itemimport.service.ItemImportService}
* in the use of the DSpace archive format which is used to
* specify changes on a per item basis. The directory names
* to correspond to each item are arbitrary and will only be
* used for logging purposes. The reference to the item is
- * from a required dc.identifier with the item handle to be
- * included in the dublin_core.xml (or similar metadata) file.
+ * from a required {@code dc.identifier} with the item handle to be
+ * included in the {@code dublin_core.xml} (or similar metadata) file.
*
- * Any combination of these actions is permitted in a single run of this class
+ *
+ * Any combination of these actions is permitted in a single run of this class.
* The order of actions is important when used in combination.
- * It is the responsibility of the calling class (here, ItemUpdate)
- * to register UpdateAction classes in the order to which they are
+ * It is the responsibility of the calling class (here, {@code ItemUpdate})
+ * to register {@link UpdateAction} classes in the order which they are
* to be performed.
*
- *
- * It is unfortunate that so much code needs to be borrowed
- * from ItemImport as it is not reusable in private methods, etc.
- * Some of this has been placed into the MetadataUtilities class
- * for possible reuse elsewhere.
+ *
+ * It is unfortunate that so much code needs to be borrowed from
+ * {@link org.dspace.app.itemimport.service.ItemImportService} as it is not
+ * reusable in private methods, etc. Some of this has been placed into the
+ * {@link MetadataUtilities} class for possible reuse elsewhere.
*
* @author W. Hays based on a conceptual design by R. Rodgers
*/
@@ -73,7 +78,7 @@ public class ItemUpdate {
public static final String DELETE_CONTENTS_FILE = "delete_contents";
public static String HANDLE_PREFIX = null;
- public static final Map filterAliases = new HashMap();
+ public static final Map filterAliases = new HashMap<>();
public static boolean verbose = false;
@@ -375,7 +380,7 @@ protected void processArchive(Context context, String sourceDirPath, String item
// open and process the source directory
File sourceDir = new File(sourceDirPath);
- if ((sourceDir == null) || !sourceDir.exists() || !sourceDir.isDirectory()) {
+ if (!sourceDir.exists() || !sourceDir.isDirectory()) {
pr("Error, cannot open archive source directory " + sourceDirPath);
throw new Exception("error with archive source directory " + sourceDirPath);
}
diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/MetadataUtilities.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/MetadataUtilities.java
index 5c2138a590d2..910eb434d1d0 100644
--- a/dspace-api/src/main/java/org/dspace/app/itemupdate/MetadataUtilities.java
+++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/MetadataUtilities.java
@@ -27,10 +27,12 @@
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
import org.apache.commons.lang3.StringUtils;
-import org.apache.xpath.XPathAPI;
-import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
@@ -170,24 +172,21 @@ public static void appendMetadata(Context context, Item item, DtoMetadata dtom,
* @param docBuilder DocumentBuilder
* @param is - InputStream of dublin_core.xml
* @return list of DtoMetadata representing the metadata fields relating to an Item
- * @throws SQLException if database error
* @throws IOException if IO error
* @throws ParserConfigurationException if parser config error
* @throws SAXException if XML error
- * @throws TransformerException if transformer error
- * @throws AuthorizeException if authorization error
*/
public static List loadDublinCore(DocumentBuilder docBuilder, InputStream is)
- throws SQLException, IOException, ParserConfigurationException,
- SAXException, TransformerException, AuthorizeException {
+ throws IOException, XPathExpressionException, SAXException {
Document document = docBuilder.parse(is);
List dtomList = new ArrayList();
// Get the schema, for backward compatibility we will default to the
// dublin core schema if the schema name is not available in the import file
- String schema = null;
- NodeList metadata = XPathAPI.selectNodeList(document, "/dublin_core");
+ String schema;
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ NodeList metadata = (NodeList) xPath.compile("/dublin_core").evaluate(document, XPathConstants.NODESET);
Node schemaAttr = metadata.item(0).getAttributes().getNamedItem("schema");
if (schemaAttr == null) {
schema = MetadataSchemaEnum.DC.getName();
@@ -196,7 +195,7 @@ public static List loadDublinCore(DocumentBuilder docBuilder, Input
}
// Get the nodes corresponding to formats
- NodeList dcNodes = XPathAPI.selectNodeList(document, "/dublin_core/dcvalue");
+ NodeList dcNodes = (NodeList) xPath.compile("/dublin_core/dcvalue").evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < dcNodes.getLength(); i++) {
Node n = dcNodes.item(i);
diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java b/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java
index ce33b6655bc6..06c2ddb48340 100644
--- a/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java
+++ b/dspace-api/src/main/java/org/dspace/app/launcher/CommandRunner.java
@@ -16,7 +16,7 @@
import java.util.ArrayList;
import java.util.List;
-import org.jdom.Document;
+import org.jdom2.Document;
/**
* @author mwood
diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java
index d445f9bbf3f5..89a416bfa883 100644
--- a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java
+++ b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java
@@ -21,6 +21,7 @@
import org.apache.logging.log4j.Logger;
import org.dspace.core.Context;
import org.dspace.scripts.DSpaceRunnable;
+import org.dspace.scripts.DSpaceRunnable.StepResult;
import org.dspace.scripts.configuration.ScriptConfiguration;
import org.dspace.scripts.factory.ScriptServiceFactory;
import org.dspace.scripts.handler.DSpaceRunnableHandler;
@@ -29,9 +30,9 @@
import org.dspace.servicemanager.DSpaceKernelImpl;
import org.dspace.servicemanager.DSpaceKernelInit;
import org.dspace.services.RequestService;
-import org.jdom.Document;
-import org.jdom.Element;
-import org.jdom.input.SAXBuilder;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.input.SAXBuilder;
/**
* A DSpace script launcher.
@@ -145,8 +146,13 @@ public static int handleScript(String[] args, Document commandConfigs,
private static int executeScript(String[] args, DSpaceRunnableHandler dSpaceRunnableHandler,
DSpaceRunnable script) {
try {
- script.initialize(args, dSpaceRunnableHandler, null);
- script.run();
+ StepResult result = script.initialize(args, dSpaceRunnableHandler, null);
+ // check the StepResult, only run the script if the result is Continue;
+ // otherwise - for example the script is started with the help as argument, nothing is to do
+ if (StepResult.Continue.equals(result)) {
+ // runs the script, the normal initialization is successful
+ script.run();
+ }
return 0;
} catch (ParseException e) {
script.printHelp();
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/Brand.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/Brand.java
index 2d963dd3da79..9e28edad45b5 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/Brand.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/Brand.java
@@ -21,10 +21,10 @@
*/
public class Brand {
- private int brandWidth;
- private int brandHeight;
- private Font font;
- private int xOffset;
+ private final int brandWidth;
+ private final int brandHeight;
+ private final Font font;
+ private final int xOffset;
/**
* Constructor to set up footer image attributes.
@@ -92,7 +92,7 @@ public BufferedImage create(String brandLeftText,
* do the text placements and preparatory work for the brand image generation
*
* @param brandImage a BufferedImage object where the image is created
- * @param identifier and Identifier object describing what text is to be placed in what
+ * @param brandText an Identifier object describing what text is to be placed in what
* position within the brand
*/
private void drawImage(BufferedImage brandImage,
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandText.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandText.java
index ae77f6048b48..91107406434e 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandText.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandText.java
@@ -39,7 +39,7 @@ class BrandText {
* its location within a rectangular area.
*
* @param location one of the class location constants e.g. Identifier.BL
- * @param the text associated with the location
+ * @param text text associated with the location
*/
public BrandText(String location, String text) {
this.location = location;
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java
deleted file mode 100644
index c17d168c0435..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ExcelFilter.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * 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.mediafilter;
-
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.logging.log4j.Logger;
-import org.apache.poi.POITextExtractor;
-import org.apache.poi.extractor.ExtractorFactory;
-import org.apache.poi.hssf.extractor.ExcelExtractor;
-import org.apache.poi.xssf.extractor.XSSFExcelExtractor;
-import org.dspace.content.Item;
-
-/*
- * ExcelFilter
- *
- * Entries you must add to dspace.cfg:
- *
- * filter.plugins = blah, \
- * Excel Text Extractor
- *
- * plugin.named.org.dspace.app.mediafilter.FormatFilter = \
- * blah = blah, \
- * org.dspace.app.mediafilter.ExcelFilter = Excel Text Extractor
- *
- * #Configure each filter's input Formats
- * filter.org.dspace.app.mediafilter.ExcelFilter.inputFormats = Microsoft Excel, Microsoft Excel XML
- *
- */
-public class ExcelFilter extends MediaFilter {
-
- private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ExcelFilter.class);
-
- public String getFilteredName(String oldFilename) {
- return oldFilename + ".txt";
- }
-
- /**
- * @return String bundle name
- */
- public String getBundleName() {
- return "TEXT";
- }
-
- /**
- * @return String bitstream format
- */
- public String getFormatString() {
- return "Text";
- }
-
- /**
- * @return String description
- */
- public String getDescription() {
- return "Extracted text";
- }
-
- /**
- * @param item item
- * @param source source input stream
- * @param verbose verbose mode
- * @return InputStream the resulting input stream
- * @throws Exception if error
- */
- @Override
- public InputStream getDestinationStream(Item item, InputStream source, boolean verbose)
- throws Exception {
- String extractedText = null;
-
- try {
- POITextExtractor theExtractor = ExtractorFactory.createExtractor(source);
- if (theExtractor instanceof ExcelExtractor) {
- // for xls file
- extractedText = (theExtractor).getText();
- } else if (theExtractor instanceof XSSFExcelExtractor) {
- // for xlsx file
- extractedText = (theExtractor).getText();
- }
- } catch (Exception e) {
- log.error("Error filtering bitstream: " + e.getMessage(), e);
- throw e;
- }
-
- if (extractedText != null) {
- // generate an input stream with the extracted text
- return IOUtils.toInputStream(extractedText, StandardCharsets.UTF_8);
- }
-
- return null;
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java
deleted file mode 100644
index 5e10f2841de5..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/HTMLFilter.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * 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.mediafilter;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import javax.swing.text.Document;
-import javax.swing.text.html.HTMLEditorKit;
-
-import org.dspace.content.Item;
-
-/*
- *
- * to do: helpful error messages - can't find mediafilter.cfg - can't
- * instantiate filter - bitstream format doesn't exist
- *
- */
-public class HTMLFilter extends MediaFilter {
-
- @Override
- public String getFilteredName(String oldFilename) {
- return oldFilename + ".txt";
- }
-
- /**
- * @return String bundle name
- */
- @Override
- public String getBundleName() {
- return "TEXT";
- }
-
- /**
- * @return String bitstream format
- */
- @Override
- public String getFormatString() {
- return "Text";
- }
-
- /**
- * @return String description
- */
- @Override
- public String getDescription() {
- return "Extracted text";
- }
-
- /**
- * @param currentItem item
- * @param source source input stream
- * @param verbose verbose mode
- * @return InputStream the resulting input stream
- * @throws Exception if error
- */
- @Override
- public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
- throws Exception {
- // try and read the document - set to ignore character set directive,
- // assuming that the input stream is already set properly (I hope)
- HTMLEditorKit kit = new HTMLEditorKit();
- Document doc = kit.createDefaultDocument();
-
- doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
-
- kit.read(source, doc, 0);
-
- String extractedText = doc.getText(0, doc.getLength());
-
- // generate an input stream with the extracted text
- byte[] textBytes = extractedText.getBytes(StandardCharsets.UTF_8);
- ByteArrayInputStream bais = new ByteArrayInputStream(textBytes);
-
- return bais;
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickPdfThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickPdfThumbnailFilter.java
index 467303c3cafd..afe1bb3d75df 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickPdfThumbnailFilter.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickPdfThumbnailFilter.java
@@ -22,7 +22,9 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo
File f2 = null;
File f3 = null;
try {
- f2 = getImageFile(f, 0, verbose);
+ // Step 1: get an image from our PDF file, with PDF-specific processing options
+ f2 = getImageFile(f, verbose);
+ // Step 2: use the image above to create the final resized and rotated thumbnail
f3 = getThumbnailFile(f2, verbose);
byte[] bytes = Files.readAllBytes(f3.toPath());
return new ByteArrayInputStream(bytes);
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java
index a79fd42d5937..408982d157e5 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java
@@ -14,6 +14,9 @@
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
@@ -113,13 +116,54 @@ public File getThumbnailFile(File f, boolean verbose)
return f2;
}
- public File getImageFile(File f, int page, boolean verbose)
+ /**
+ * Return an image from a bitstream with specific processing options for
+ * PDFs. This is only used by ImageMagickPdfThumbnailFilter in order to
+ * generate an intermediate image file for use with getThumbnailFile.
+ */
+ public File getImageFile(File f, boolean verbose)
throws IOException, InterruptedException, IM4JavaException {
- File f2 = new File(f.getParentFile(), f.getName() + ".jpg");
+ // Writing an intermediate file to disk is inefficient, but since we're
+ // doing it anyway, we should use a lossless format. IM's internal MIFF
+ // is lossless like PNG and TIFF, but much faster.
+ File f2 = new File(f.getParentFile(), f.getName() + ".miff");
f2.deleteOnExit();
ConvertCmd cmd = new ConvertCmd();
IMOperation op = new IMOperation();
- String s = "[" + page + "]";
+
+ // Optionally override ImageMagick's default density of 72 DPI to use a
+ // "supersample" when creating the PDF thumbnail. Note that I prefer to
+ // use the getProperty() method here instead of getIntPropert() because
+ // the latter always returns an integer (0 in the case it's not set). I
+ // would prefer to keep ImageMagick's default to itself rather than for
+ // us to set one. Also note that the density option *must* come before
+ // we open the input file.
+ String density = configurationService.getProperty(PRE + ".density");
+ if (density != null) {
+ op.density(Integer.valueOf(density));
+ }
+
+ // Check the PDF's MediaBox and CropBox to see if they are the same.
+ // If not, then tell ImageMagick to use the CropBox when generating
+ // the thumbnail because the CropBox is generally used to define the
+ // area displayed when a user opens the PDF on a screen, whereas the
+ // MediaBox is used for print. Not all PDFs set these correctly, so
+ // we can use ImageMagick's default behavior unless we see an explit
+ // CropBox. Note: we don't need to do anything special to detect if
+ // the CropBox is missing or empty because pdfbox will set it to the
+ // same size as the MediaBox if it doesn't exist. Also note that we
+ // only need to check the first page, since that's what we use for
+ // generating the thumbnail (PDDocument uses a zero-based index).
+ PDPage pdfPage = PDDocument.load(f).getPage(0);
+ PDRectangle pdfPageMediaBox = pdfPage.getMediaBox();
+ PDRectangle pdfPageCropBox = pdfPage.getCropBox();
+
+ // This option must come *before* we open the input file.
+ if (pdfPageCropBox != pdfPageMediaBox) {
+ op.define("pdf:use-cropbox=true");
+ }
+
+ String s = "[0]";
op.addImage(f.getAbsolutePath() + s);
if (configurationService.getBooleanProperty(PRE + ".flatten", true)) {
op.flatten();
@@ -172,20 +216,20 @@ public boolean preProcessBitstream(Context c, Item item, Bitstream source, boole
if (description != null) {
if (replaceRegex.matcher(description).matches()) {
if (verbose) {
- System.out.format("%s %s matches pattern and is replacable.%n",
- description, nsrc);
+ System.out.format("%s %s matches pattern and is replaceable.%n",
+ description, n);
}
continue;
}
if (description.equals(getDescription())) {
if (verbose) {
System.out.format("%s %s is replaceable.%n",
- getDescription(), nsrc);
+ getDescription(), n);
}
continue;
}
}
- System.out.format("Custom Thumbnail exists for %s for item %s. Thumbnail will not be generated.%n",
+ System.out.format("Custom thumbnail exists for %s for item %s. Thumbnail will not be generated.%n",
nsrc, item.getHandle());
return false;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickVideoThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickVideoThumbnailFilter.java
new file mode 100644
index 000000000000..4221a514d7d5
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickVideoThumbnailFilter.java
@@ -0,0 +1,76 @@
+/**
+ * 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.mediafilter;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+import org.dspace.content.Item;
+import org.im4java.core.ConvertCmd;
+import org.im4java.core.IM4JavaException;
+import org.im4java.core.IMOperation;
+
+
+/**
+ * Filter video bitstreams, scaling the image to be within the bounds of
+ * thumbnail.maxwidth, thumbnail.maxheight, the size we want our thumbnail to be
+ * no bigger than. Creates only JPEGs.
+ */
+public class ImageMagickVideoThumbnailFilter extends ImageMagickThumbnailFilter {
+ private static final int DEFAULT_WIDTH = 180;
+ private static final int DEFAULT_HEIGHT = 120;
+ private static final int FRAME_NUMBER = 100;
+
+ /**
+ * @param currentItem item
+ * @param source source input stream
+ * @param verbose verbose mode
+ * @return InputStream the resulting input stream
+ * @throws Exception if error
+ */
+ @Override
+ public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
+ throws Exception {
+ File f = inputStreamToTempFile(source, "imthumb", ".tmp");
+ File f2 = null;
+ try {
+ f2 = getThumbnailFile(f, verbose);
+ byte[] bytes = Files.readAllBytes(f2.toPath());
+ return new ByteArrayInputStream(bytes);
+ } finally {
+ //noinspection ResultOfMethodCallIgnored
+ f.delete();
+ if (f2 != null) {
+ //noinspection ResultOfMethodCallIgnored
+ f2.delete();
+ }
+ }
+ }
+
+ @Override
+ public File getThumbnailFile(File f, boolean verbose)
+ throws IOException, InterruptedException, IM4JavaException {
+ File f2 = new File(f.getParentFile(), f.getName() + ".jpg");
+ f2.deleteOnExit();
+ ConvertCmd cmd = new ConvertCmd();
+ IMOperation op = new IMOperation();
+ op.autoOrient();
+ op.addImage("VIDEO:" + f.getAbsolutePath() + "[" + FRAME_NUMBER + "]");
+ op.thumbnail(configurationService.getIntProperty("thumbnail.maxwidth", DEFAULT_WIDTH),
+ configurationService.getIntProperty("thumbnail.maxheight", DEFAULT_HEIGHT));
+ op.addImage(f2.getAbsolutePath());
+ if (verbose) {
+ System.out.println("IM Thumbnail Param: " + op);
+ }
+ cmd.run(op);
+ return f2;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java
index 49ee23b924b1..867e684db86b 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java
@@ -7,25 +7,16 @@
*/
package org.dspace.app.mediafilter;
-import java.sql.SQLException;
-
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
-import org.dspace.authorize.service.AuthorizeService;
-import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
-import org.springframework.beans.factory.annotation.Autowired;
public class MediaFilterScriptConfiguration extends ScriptConfiguration {
- @Autowired
- private AuthorizeService authorizeService;
-
private Class dspaceRunnableClass;
private static final String MEDIA_FILTER_PLUGINS_KEY = "filter.plugins";
-
@Override
public Class getDspaceRunnableClass() {
return dspaceRunnableClass;
@@ -36,29 +27,15 @@ public void setDspaceRunnableClass(Class dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
-
- @Override
- public boolean isAllowedToExecute(final Context context) {
- try {
- return authorizeService.isAdmin(context);
- } catch (SQLException e) {
- throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
- }
- }
-
@Override
public Options getOptions() {
Options options = new Options();
options.addOption("v", "verbose", false, "print all extracted text and other details to STDOUT");
- options.getOption("v").setType(boolean.class);
options.addOption("q", "quiet", false, "do not print anything except in the event of errors.");
- options.getOption("q").setType(boolean.class);
options.addOption("f", "force", false, "force all bitstreams to be processed");
- options.getOption("f").setType(boolean.class);
options.addOption("i", "identifier", true, "ONLY process bitstreams belonging to identifier");
options.addOption("m", "maximum", true, "process no more than maximum items");
options.addOption("h", "help", false, "help");
- options.getOption("h").setType(boolean.class);
Option pluginOption = Option.builder("p")
.longOpt("plugins")
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java
index 50efa68ff410..b50fb22355a3 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java
@@ -8,13 +8,18 @@
package org.dspace.app.mediafilter;
import java.io.InputStream;
+import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
import org.dspace.app.mediafilter.service.MediaFilterService;
+import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
@@ -36,6 +41,7 @@
import org.dspace.eperson.service.GroupService;
import org.dspace.scripts.handler.DSpaceRunnableHandler;
import org.dspace.services.ConfigurationService;
+import org.dspace.util.ThrowableUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
@@ -221,23 +227,9 @@ public boolean filterBitstream(Context context, Item myItem,
filtered = true;
}
} catch (Exception e) {
- String handle = myItem.getHandle();
- List bundles = myBitstream.getBundles();
- long size = myBitstream.getSizeBytes();
- String checksum = myBitstream.getChecksum() + " (" + myBitstream.getChecksumAlgorithm() + ")";
- int assetstore = myBitstream.getStoreNumber();
-
// Printout helpful information to find the errored bitstream.
- StringBuilder sb = new StringBuilder("ERROR filtering, skipping bitstream:\n");
- sb.append("\tItem Handle: ").append(handle);
- for (Bundle bundle : bundles) {
- sb.append("\tBundle Name: ").append(bundle.getName());
- }
- sb.append("\tFile Size: ").append(size);
- sb.append("\tChecksum: ").append(checksum);
- sb.append("\tAsset Store: ").append(assetstore);
- logError(sb.toString());
- logError(e.getMessage(), e);
+ logError(formatBitstreamDetails(myItem.getHandle(), myBitstream));
+ logError(ThrowableUtils.formatCauseChain(e));
}
} else if (filterClass instanceof SelfRegisterInputFormats) {
// Filter implements self registration, so check to see if it should be applied
@@ -315,25 +307,25 @@ public boolean processBitstream(Context context, Item item, Bitstream source, Fo
// check if destination bitstream exists
Bundle existingBundle = null;
- Bitstream existingBitstream = null;
+ List existingBitstreams = new ArrayList<>();
List bundles = itemService.getBundles(item, formatFilter.getBundleName());
- if (bundles.size() > 0) {
- // only finds the last match (FIXME?)
+ if (!bundles.isEmpty()) {
+ // only finds the last matching bundle and all matching bitstreams in the proper bundle(s)
for (Bundle bundle : bundles) {
List bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
if (bitstream.getName().trim().equals(newName.trim())) {
existingBundle = bundle;
- existingBitstream = bitstream;
+ existingBitstreams.add(bitstream);
}
}
}
}
// if exists and overwrite = false, exit
- if (!overWrite && (existingBitstream != null)) {
+ if (!overWrite && (!existingBitstreams.isEmpty())) {
if (!isQuiet) {
logInfo("SKIPPED: bitstream " + source.getID()
+ " (item: " + item.getHandle() + ") because '" + newName + "' already exists");
@@ -366,7 +358,7 @@ public boolean processBitstream(Context context, Item item, Bitstream source, Fo
}
Bundle targetBundle; // bundle we're modifying
- if (bundles.size() < 1) {
+ if (bundles.isEmpty()) {
// create new bundle if needed
targetBundle = bundleService.create(context, item, formatFilter.getBundleName());
} else {
@@ -388,29 +380,18 @@ public boolean processBitstream(Context context, Item item, Bitstream source, Fo
bitstreamService.update(context, b);
//Set permissions on the derivative bitstream
- //- First remove any existing policies
- authorizeService.removeAllPolicies(context, b);
-
- //- Determine if this is a public-derivative format
- if (publicFiltersClasses.contains(formatFilter.getClass().getSimpleName())) {
- //- Set derivative bitstream to be publicly accessible
- Group anonymous = groupService.findByName(context, Group.ANONYMOUS);
- authorizeService.addPolicy(context, b, Constants.READ, anonymous);
- } else {
- //- Inherit policies from the source bitstream
- authorizeService.inheritPolicies(context, source, b);
- }
+ updatePoliciesOfDerivativeBitstream(context, b, formatFilter, source);
//do post-processing of the generated bitstream
formatFilter.postProcessBitstream(context, item, b);
} catch (OutOfMemoryError oome) {
logError("!!! OutOfMemoryError !!!");
+ logError(formatBitstreamDetails(item.getHandle(), source));
}
- // fixme - set date?
// we are overwriting, so remove old bitstream
- if (existingBitstream != null) {
+ for (Bitstream existingBitstream : existingBitstreams) {
bundleService.removeBitstream(context, existingBundle, existingBitstream);
}
@@ -422,6 +403,71 @@ public boolean processBitstream(Context context, Item item, Bitstream source, Fo
return true;
}
+ @Override
+ public void updatePoliciesOfDerivativeBitstreams(Context context, Item item, Bitstream source)
+ throws SQLException, AuthorizeException {
+
+ if (filterClasses == null) {
+ return;
+ }
+
+ for (FormatFilter formatFilter : filterClasses) {
+ for (Bitstream bitstream : findDerivativeBitstreams(item, source, formatFilter)) {
+ updatePoliciesOfDerivativeBitstream(context, bitstream, formatFilter, source);
+ }
+ }
+ }
+
+ /**
+ * find derivative bitstreams related to source bitstream
+ *
+ * @param item item containing bitstreams
+ * @param source source bitstream
+ * @param formatFilter formatFilter
+ * @return list of derivative bitstreams from source bitstream
+ * @throws SQLException If something goes wrong in the database
+ */
+ private List findDerivativeBitstreams(Item item, Bitstream source, FormatFilter formatFilter)
+ throws SQLException {
+
+ String bitstreamName = formatFilter.getFilteredName(source.getName());
+ List bundles = itemService.getBundles(item, formatFilter.getBundleName());
+
+ return bundles.stream()
+ .flatMap(bundle ->
+ bundle.getBitstreams().stream())
+ .filter(bitstream ->
+ StringUtils.equals(bitstream.getName().trim(), bitstreamName.trim()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * update resource polices of derivative bitstreams.
+ * by remove all resource policies and
+ * set derivative bitstreams to be publicly accessible or
+ * replace derivative bitstreams policies using
+ * the same in the source bitstream.
+ *
+ * @param context the context
+ * @param bitstream derivative bitstream
+ * @param formatFilter formatFilter
+ * @param source the source bitstream
+ * @throws SQLException If something goes wrong in the database
+ * @throws AuthorizeException if authorization error
+ */
+ private void updatePoliciesOfDerivativeBitstream(Context context, Bitstream bitstream, FormatFilter formatFilter,
+ Bitstream source) throws SQLException, AuthorizeException {
+
+ authorizeService.removeAllPolicies(context, bitstream);
+
+ if (publicFiltersClasses.contains(formatFilter.getClass().getSimpleName())) {
+ Group anonymous = groupService.findByName(context, Group.ANONYMOUS);
+ authorizeService.addPolicy(context, bitstream, Constants.READ, anonymous);
+ } else {
+ authorizeService.replaceAllPolicies(context, source, bitstream);
+ }
+ }
+
@Override
public Item getCurrentItem() {
return currentItem;
@@ -439,6 +485,37 @@ public boolean inSkipList(String identifier) {
}
}
+ /**
+ * Describe a Bitstream in detail. Format a single line of text with
+ * information such as Bitstore index, backing file ID, size, checksum,
+ * enclosing Item and Bundles.
+ *
+ * @param itemHandle Handle of the Item by which we found the Bitstream.
+ * @param bitstream the Bitstream to be described.
+ * @return Bitstream details.
+ */
+ private String formatBitstreamDetails(String itemHandle,
+ Bitstream bitstream) {
+ List bundles;
+ try {
+ bundles = bitstream.getBundles();
+ } catch (SQLException ex) {
+ logError("Unexpected error fetching Bundles", ex);
+ bundles = Collections.EMPTY_LIST;
+ }
+ StringBuilder sb = new StringBuilder("ERROR filtering, skipping bitstream:\n");
+ sb.append("\tItem Handle: ").append(itemHandle);
+ for (Bundle bundle : bundles) {
+ sb.append("\tBundle Name: ").append(bundle.getName());
+ }
+ sb.append("\tFile Size: ").append(bitstream.getSizeBytes());
+ sb.append("\tChecksum: ").append(bitstream.getChecksum())
+ .append(" (").append(bitstream.getChecksumAlgorithm()).append(')');
+ sb.append("\tAsset Store: ").append(bitstream.getStoreNumber());
+ sb.append("\tInternal ID: ").append(bitstream.getInternalId());
+ return sb.toString();
+ }
+
private void logInfo(String message) {
if (handler != null) {
handler.logInfo(message);
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java
deleted file mode 100644
index c90d7c5a6e97..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFFilter.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * 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.mediafilter;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-
-import org.apache.logging.log4j.Logger;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
-import org.apache.pdfbox.text.PDFTextStripper;
-import org.dspace.content.Item;
-import org.dspace.services.ConfigurationService;
-import org.dspace.services.factory.DSpaceServicesFactory;
-
-/*
- *
- * to do: helpful error messages - can't find mediafilter.cfg - can't
- * instantiate filter - bitstream format doesn't exist
- *
- */
-public class PDFFilter extends MediaFilter {
-
- private static Logger log = org.apache.logging.log4j.LogManager.getLogger(PDFFilter.class);
-
- @Override
- public String getFilteredName(String oldFilename) {
- return oldFilename + ".txt";
- }
-
- /**
- * @return String bundle name
- */
- @Override
- public String getBundleName() {
- return "TEXT";
- }
-
- /**
- * @return String bitstreamformat
- */
- @Override
- public String getFormatString() {
- return "Text";
- }
-
- /**
- * @return String description
- */
- @Override
- public String getDescription() {
- return "Extracted text";
- }
-
- /**
- * @param currentItem item
- * @param source source input stream
- * @param verbose verbose mode
- * @return InputStream the resulting input stream
- * @throws Exception if error
- */
- @Override
- public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
- throws Exception {
- ConfigurationService configurationService
- = DSpaceServicesFactory.getInstance().getConfigurationService();
- try {
- boolean useTemporaryFile = configurationService.getBooleanProperty("pdffilter.largepdfs", false);
-
- // get input stream from bitstream
- // pass to filter, get string back
- PDFTextStripper pts = new PDFTextStripper();
- pts.setSortByPosition(true);
- PDDocument pdfDoc = null;
- Writer writer = null;
- File tempTextFile = null;
- ByteArrayOutputStream byteStream = null;
-
- if (useTemporaryFile) {
- tempTextFile = File.createTempFile("dspacepdfextract" + source.hashCode(), ".txt");
- tempTextFile.deleteOnExit();
- writer = new OutputStreamWriter(new FileOutputStream(tempTextFile));
- } else {
- byteStream = new ByteArrayOutputStream();
- writer = new OutputStreamWriter(byteStream);
- }
-
- try {
- pdfDoc = PDDocument.load(source);
- pts.writeText(pdfDoc, writer);
- } catch (InvalidPasswordException ex) {
- log.error("PDF is encrypted. Cannot extract text (item: {})",
- () -> currentItem.getHandle());
- return null;
- } finally {
- try {
- if (pdfDoc != null) {
- pdfDoc.close();
- }
- } catch (Exception e) {
- log.error("Error closing PDF file: " + e.getMessage(), e);
- }
-
- try {
- writer.close();
- } catch (Exception e) {
- log.error("Error closing temporary extract file: " + e.getMessage(), e);
- }
- }
-
- if (useTemporaryFile) {
- return new FileInputStream(tempTextFile);
- } else {
- byte[] bytes = byteStream.toByteArray();
- return new ByteArrayInputStream(bytes);
- }
- } catch (OutOfMemoryError oome) {
- log.error("Error parsing PDF document " + oome.getMessage(), oome);
- if (!configurationService.getBooleanProperty("pdffilter.skiponmemoryexception", false)) {
- throw oome;
- }
- }
-
- return null;
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java
deleted file mode 100644
index 8c198c447768..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PoiWordFilter.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * 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.mediafilter;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-
-import org.apache.poi.POITextExtractor;
-import org.apache.poi.extractor.ExtractorFactory;
-import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
-import org.apache.xmlbeans.XmlException;
-import org.dspace.content.Item;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Extract flat text from Microsoft Word documents (.doc, .docx).
- */
-public class PoiWordFilter
- extends MediaFilter {
- private static final Logger LOG = LoggerFactory.getLogger(PoiWordFilter.class);
-
- @Override
- public String getFilteredName(String oldFilename) {
- return oldFilename + ".txt";
- }
-
- @Override
- public String getBundleName() {
- return "TEXT";
- }
-
- @Override
- public String getFormatString() {
- return "Text";
- }
-
- @Override
- public String getDescription() {
- return "Extracted text";
- }
-
- @Override
- public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
- throws Exception {
- String text;
- try {
- // get input stream from bitstream, pass to filter, get string back
- POITextExtractor extractor = ExtractorFactory.createExtractor(source);
- text = extractor.getText();
- } catch (IOException | OpenXML4JException | XmlException e) {
- System.err.format("Invalid File Format: %s%n", e.getMessage());
- LOG.error("Unable to parse the bitstream: ", e);
- throw e;
- }
-
- // if verbose flag is set, print out extracted text to STDOUT
- if (verbose) {
- System.out.println(text);
- }
-
- // return the extracted text as a stream.
- return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java
deleted file mode 100644
index 86b7096f68f9..000000000000
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PowerPointFilter.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * 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.mediafilter;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-import org.apache.logging.log4j.Logger;
-import org.apache.poi.POITextExtractor;
-import org.apache.poi.extractor.ExtractorFactory;
-import org.apache.poi.hslf.extractor.PowerPointExtractor;
-import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor;
-import org.dspace.content.Item;
-
-/*
- * TODO: Allow user to configure extraction of only text or only notes
- *
- */
-public class PowerPointFilter extends MediaFilter {
-
- private static Logger log = org.apache.logging.log4j.LogManager.getLogger(PowerPointFilter.class);
-
- @Override
- public String getFilteredName(String oldFilename) {
- return oldFilename + ".txt";
- }
-
- /**
- * @return String bundle name
- */
- @Override
- public String getBundleName() {
- return "TEXT";
- }
-
- /**
- * @return String bitstream format
- *
- * TODO: Check that this is correct
- */
- @Override
- public String getFormatString() {
- return "Text";
- }
-
- /**
- * @return String description
- */
- @Override
- public String getDescription() {
- return "Extracted text";
- }
-
- /**
- * @param currentItem item
- * @param source source input stream
- * @param verbose verbose mode
- * @return InputStream the resulting input stream
- * @throws Exception if error
- */
- @Override
- public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
- throws Exception {
-
- try {
-
- String extractedText = null;
- new ExtractorFactory();
- POITextExtractor pptExtractor = ExtractorFactory
- .createExtractor(source);
-
- // PowerPoint XML files and legacy format PowerPoint files
- // require different classes and APIs for text extraction
-
- // If this is a PowerPoint XML file, extract accordingly
- if (pptExtractor instanceof XSLFPowerPointExtractor) {
-
- // The true method arguments indicate that text from
- // the slides and the notes is desired
- extractedText = ((XSLFPowerPointExtractor) pptExtractor)
- .getText(true, true);
- } else if (pptExtractor instanceof PowerPointExtractor) { // Legacy PowerPoint files
-
- extractedText = ((PowerPointExtractor) pptExtractor).getText()
- + " " + ((PowerPointExtractor) pptExtractor).getNotes();
-
- }
- if (extractedText != null) {
- // if verbose flag is set, print out extracted text
- // to STDOUT
- if (verbose) {
- System.out.println(extractedText);
- }
-
- // generate an input stream with the extracted text
- byte[] textBytes = extractedText.getBytes();
- ByteArrayInputStream bais = new ByteArrayInputStream(textBytes);
-
- return bais;
- }
- } catch (Exception e) {
- log.error("Error filtering bitstream: " + e.getMessage(), e);
- throw e;
- }
-
- return null;
- }
-}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java
new file mode 100644
index 000000000000..e83bf706ed02
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java
@@ -0,0 +1,183 @@
+/**
+ * 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.mediafilter;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.tika.Tika;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.sax.BodyContentHandler;
+import org.apache.tika.sax.ContentHandlerDecorator;
+import org.dspace.content.Item;
+import org.dspace.services.ConfigurationService;
+import org.dspace.services.factory.DSpaceServicesFactory;
+import org.xml.sax.SAXException;
+
+/**
+ * Text Extraction media filter which uses Apache Tika to extract text from a large number of file formats (including
+ * all Microsoft formats, PDF, HTML, Text, etc). For a more complete list of file formats supported by Tika see the
+ * Tika documentation: https://tika.apache.org/2.3.0/formats.html
+ */
+public class TikaTextExtractionFilter
+ extends MediaFilter {
+ private final static Logger log = LogManager.getLogger();
+
+ @Override
+ public String getFilteredName(String oldFilename) {
+ return oldFilename + ".txt";
+ }
+
+ @Override
+ public String getBundleName() {
+ return "TEXT";
+ }
+
+ @Override
+ public String getFormatString() {
+ return "Text";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Extracted text";
+ }
+
+ @Override
+ public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose)
+ throws Exception {
+ ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
+ boolean useTemporaryFile = configurationService.getBooleanProperty("textextractor.use-temp-file", false);
+
+ if (useTemporaryFile) {
+ // Extract text out of source file using a temp file, returning results as InputStream
+ return extractUsingTempFile(source, verbose);
+ }
+
+ // Not using temporary file. We'll use Tika's default in-memory parsing.
+ // Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting.
+ String extractedText;
+ int maxChars = configurationService.getIntProperty("textextractor.max-chars", 100000);
+ try {
+ // Use Tika to extract text from input. Tika will automatically detect the file type.
+ Tika tika = new Tika();
+ tika.setMaxStringLength(maxChars); // Tell Tika the maximum number of characters to extract
+ extractedText = tika.parseToString(source);
+ } catch (IOException e) {
+ System.err.format("Unable to extract text from bitstream in Item %s%n", currentItem.getID().toString());
+ e.printStackTrace();
+ log.error("Unable to extract text from bitstream in Item {}", currentItem.getID().toString(), e);
+ throw e;
+ } catch (OutOfMemoryError oe) {
+ System.err.format("OutOfMemoryError occurred when extracting text from bitstream in Item %s. " +
+ "You may wish to enable 'textextractor.use-temp-file'.%n", currentItem.getID().toString());
+ oe.printStackTrace();
+ log.error("OutOfMemoryError occurred when extracting text from bitstream in Item {}. " +
+ "You may wish to enable 'textextractor.use-temp-file'.", currentItem.getID().toString(), oe);
+ throw oe;
+ }
+
+ if (StringUtils.isNotEmpty(extractedText)) {
+ // if verbose flag is set, print out extracted text to STDOUT
+ if (verbose) {
+ System.out.println("(Verbose mode) Extracted text:");
+ System.out.println(extractedText);
+ }
+
+ // return the extracted text as a UTF-8 stream.
+ return new ByteArrayInputStream(extractedText.getBytes(StandardCharsets.UTF_8));
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the text out of a given source InputStream, using a temporary file. This decreases the amount of memory
+ * necessary for text extraction, but can be slower as it requires writing extracted text to a temporary file.
+ * @param source source InputStream
+ * @param verbose verbose mode enabled/disabled
+ * @return InputStream for temporary file containing extracted text
+ * @throws IOException
+ * @throws SAXException
+ * @throws TikaException
+ */
+ private InputStream extractUsingTempFile(InputStream source, boolean verbose)
+ throws IOException, TikaException, SAXException {
+ File tempExtractedTextFile = File.createTempFile("dspacetextextract" + source.hashCode(), ".txt");
+
+ if (verbose) {
+ System.out.println("(Verbose mode) Extracted text was written to temporary file at " +
+ tempExtractedTextFile.getAbsolutePath());
+ } else {
+ tempExtractedTextFile.deleteOnExit();
+ }
+
+ // Open temp file for writing
+ try (FileWriter writer = new FileWriter(tempExtractedTextFile, StandardCharsets.UTF_8)) {
+ // Initialize a custom ContentHandlerDecorator which is a BodyContentHandler.
+ // This mimics the behavior of Tika().parseToString(), which only extracts text from the body of the file.
+ // This custom Handler writes any extracted text to the temp file.
+ ContentHandlerDecorator handler = new BodyContentHandler(new ContentHandlerDecorator() {
+ /**
+ * Write all extracted characters directly to the temp file.
+ */
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ try {
+ writer.append(new String(ch), start, length);
+ } catch (IOException e) {
+ String errorMsg = String.format("Could not append to temporary file at %s " +
+ "when performing text extraction",
+ tempExtractedTextFile.getAbsolutePath());
+ log.error(errorMsg, e);
+ throw new SAXException(errorMsg, e);
+ }
+ }
+
+ /**
+ * Write all ignorable whitespace directly to the temp file.
+ * This mimics the behaviour of Tika().parseToString() which extracts ignorableWhitespace characters
+ * (like blank lines, indentations, etc.), so that we get the same extracted text either way.
+ */
+ @Override
+ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
+ try {
+ writer.append(new String(ch), start, length);
+ } catch (IOException e) {
+ String errorMsg = String.format("Could not append to temporary file at %s " +
+ "when performing text extraction",
+ tempExtractedTextFile.getAbsolutePath());
+ log.error(errorMsg, e);
+ throw new SAXException(errorMsg, e);
+ }
+ }
+ });
+
+ AutoDetectParser parser = new AutoDetectParser();
+ Metadata metadata = new Metadata();
+ // parse our source InputStream using the above custom handler
+ parser.parse(source, handler, metadata);
+ }
+
+ // At this point, all extracted text is written to our temp file. So, return a FileInputStream for that file
+ return new FileInputStream(tempExtractedTextFile);
+ }
+
+
+
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java
index 50a6bb3a2027..bc92ff521098 100644
--- a/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java
+++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/service/MediaFilterService.java
@@ -7,10 +7,12 @@
*/
package org.dspace.app.mediafilter.service;
+import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import org.dspace.app.mediafilter.FormatFilter;
+import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.Collection;
import org.dspace.content.Community;
@@ -91,6 +93,22 @@ public void applyFiltersCollection(Context context, Collection collection)
public boolean processBitstream(Context context, Item item, Bitstream source, FormatFilter formatFilter)
throws Exception;
+ /**
+ * update resource polices of derivative bitstreams
+ * related to source bitstream.
+ * set derivative bitstreams to be publicly accessible or
+ * replace derivative bitstreams policies using
+ * the same in the source bitstream.
+ *
+ * @param context context
+ * @param item item containing bitstreams
+ * @param source source bitstream
+ * @throws SQLException If something goes wrong in the database
+ * @throws AuthorizeException if authorization error
+ */
+ public void updatePoliciesOfDerivativeBitstreams(Context context, Item item, Bitstream source)
+ throws SQLException, AuthorizeException;
+
/**
* Return the item that is currently being processed/filtered
* by the MediaFilterManager.
diff --git a/dspace-api/src/main/java/org/dspace/app/packager/Packager.java b/dspace-api/src/main/java/org/dspace/app/packager/Packager.java
index 0e985bd244ae..21d156268609 100644
--- a/dspace-api/src/main/java/org/dspace/app/packager/Packager.java
+++ b/dspace-api/src/main/java/org/dspace/app/packager/Packager.java
@@ -631,7 +631,7 @@ protected void disseminate(Context context, PackageDisseminator dip,
//otherwise, just disseminate a single object to a single package file
dip.disseminate(context, dso, pkgParams, pkgFile);
- if (pkgFile != null && pkgFile.exists()) {
+ if (pkgFile.exists()) {
System.out.println("\nCREATED package file: " + pkgFile.getCanonicalPath());
}
}
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java
new file mode 100644
index 000000000000..135406069ae3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java
@@ -0,0 +1,46 @@
+/**
+ * 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.requestitem;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dspace.content.Collection;
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.dspace.eperson.EPerson;
+import org.springframework.lang.NonNull;
+
+/**
+ * Derive request recipients from groups of the Collection which owns an Item.
+ * The list will include all members of the administrators group. If the
+ * resulting list is empty, delegates to {@link RequestItemHelpdeskStrategy}.
+ *
+ * @author Mark H. Wood
+ */
+public class CollectionAdministratorsRequestItemStrategy
+ extends RequestItemHelpdeskStrategy {
+ @Override
+ @NonNull
+ public List getRequestItemAuthor(Context context,
+ Item item)
+ throws SQLException {
+ List recipients = new ArrayList<>();
+ Collection collection = item.getOwningCollection();
+ for (EPerson admin : collection.getAdministrators().getMembers()) {
+ recipients.add(new RequestItemAuthor(admin));
+ }
+ if (recipients.isEmpty()) {
+ return super.getRequestItemAuthor(context, item);
+ } else {
+ return recipients;
+ }
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java
new file mode 100644
index 000000000000..8292c1a72835
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/CombiningRequestItemStrategy.java
@@ -0,0 +1,61 @@
+/**
+ * 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.requestitem;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dspace.content.Item;
+import org.dspace.core.Context;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+
+/**
+ * Assemble a list of recipients from the results of other strategies.
+ * The list of strategy classes is injected as the constructor argument
+ * {@code strategies}.
+ * If the strategy list is not configured, returns an empty List.
+ *
+ * @author Mark H. Wood
+ */
+public class CombiningRequestItemStrategy
+ implements RequestItemAuthorExtractor {
+ /** The strategies to combine. */
+ private final List strategies;
+
+ /**
+ * Initialize a combination of strategies.
+ * @param strategies the author extraction strategies to combine.
+ */
+ public CombiningRequestItemStrategy(@NonNull List strategies) {
+ Assert.notNull(strategies, "Strategy list may not be null");
+ this.strategies = strategies;
+ }
+
+ /**
+ * Do not call.
+ * @throws IllegalArgumentException always
+ */
+ private CombiningRequestItemStrategy() {
+ throw new IllegalArgumentException();
+ }
+
+ @Override
+ @NonNull
+ public List getRequestItemAuthor(Context context, Item item)
+ throws SQLException {
+ List recipients = new ArrayList<>();
+
+ for (RequestItemAuthorExtractor strategy : strategies) {
+ recipients.addAll(strategy.getRequestItemAuthor(context, item));
+ }
+
+ return recipients;
+ }
+}
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
index 9e675e97a7e6..cdefd1298c6e 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItem.java
@@ -27,7 +27,7 @@
import org.dspace.core.ReloadableEntity;
/**
- * Object representing an Item Request
+ * Object representing an Item Request.
*/
@Entity
@Table(name = "requestitem")
@@ -94,6 +94,9 @@ void setAllfiles(boolean allfiles) {
this.allfiles = allfiles;
}
+ /**
+ * @return {@code true} if all of the Item's files are requested.
+ */
public boolean isAllfiles() {
return allfiles;
}
@@ -102,6 +105,9 @@ void setReqMessage(String reqMessage) {
this.reqMessage = reqMessage;
}
+ /**
+ * @return a message from the requester.
+ */
public String getReqMessage() {
return reqMessage;
}
@@ -110,6 +116,9 @@ void setReqName(String reqName) {
this.reqName = reqName;
}
+ /**
+ * @return Human-readable name of the user requesting access.
+ */
public String getReqName() {
return reqName;
}
@@ -118,6 +127,9 @@ void setReqEmail(String reqEmail) {
this.reqEmail = reqEmail;
}
+ /**
+ * @return address of the user requesting access.
+ */
public String getReqEmail() {
return reqEmail;
}
@@ -126,6 +138,9 @@ void setToken(String token) {
this.token = token;
}
+ /**
+ * @return a unique request identifier which can be emailed.
+ */
public String getToken() {
return token;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java
index 49e26fe00bd3..a189e4a5efdd 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthor.java
@@ -11,20 +11,31 @@
/**
* Simple DTO to transfer data about the corresponding author for the Request
- * Copy feature
+ * Copy feature.
*
* @author Andrea Bollini
*/
public class RequestItemAuthor {
- private String fullName;
- private String email;
+ private final String fullName;
+ private final String email;
+ /**
+ * Construct an author record from given data.
+ *
+ * @param fullName the author's full name.
+ * @param email the author's email address.
+ */
public RequestItemAuthor(String fullName, String email) {
super();
this.fullName = fullName;
this.email = email;
}
+ /**
+ * Construct an author from an EPerson's metadata.
+ *
+ * @param ePerson the EPerson.
+ */
public RequestItemAuthor(EPerson ePerson) {
super();
this.fullName = ePerson.getFullName();
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java
index 9b66030e9030..5c6e48ee3f85 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemAuthorExtractor.java
@@ -8,26 +8,28 @@
package org.dspace.app.requestitem;
import java.sql.SQLException;
+import java.util.List;
import org.dspace.content.Item;
import org.dspace.core.Context;
+import org.springframework.lang.NonNull;
/**
- * Interface to abstract the strategy for select the author to contact for
- * request copy
+ * Interface to abstract the strategy for selecting the author to contact for
+ * request copy.
*
* @author Andrea Bollini
*/
public interface RequestItemAuthorExtractor {
-
/**
- * Retrieve the auhtor to contact for a request copy of the give item.
+ * Retrieve the author to contact for requesting a copy of the given item.
*
* @param context DSpace context object
* @param item item to request
- * @return An object containing name an email address to send the request to
- * or null if no valid email address was found.
+ * @return Names and email addresses to send the request to.
* @throws SQLException if database error
*/
- public RequestItemAuthor getRequestItemAuthor(Context context, Item item) throws SQLException;
+ @NonNull
+ public List getRequestItemAuthor(Context context, Item item)
+ throws SQLException;
}
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
index d72e42eac183..6499c45a7830 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java
@@ -11,54 +11,59 @@
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
+import javax.annotation.ManagedBean;
+import javax.inject.Inject;
+import javax.inject.Singleton;
import javax.mail.MessagingException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
import org.dspace.app.requestitem.service.RequestItemService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
-import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Context;
import org.dspace.core.Email;
import org.dspace.core.I18nUtil;
import org.dspace.core.LogHelper;
import org.dspace.eperson.EPerson;
-import org.dspace.handle.factory.HandleServiceFactory;
import org.dspace.handle.service.HandleService;
import org.dspace.services.ConfigurationService;
-import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Send item requests and responses by email.
*
+ * The "strategy" by which approvers are chosen is in an implementation of
+ * {@link RequestItemAuthorExtractor} which is injected by the name
+ * {@code requestItemAuthorExtractor}. See the DI configuration documents.
+ *
* @author Mark H. Wood
*/
+@Singleton
+@ManagedBean
public class RequestItemEmailNotifier {
private static final Logger LOG = LogManager.getLogger();
- private static final BitstreamService bitstreamService
- = ContentServiceFactory.getInstance().getBitstreamService();
+ @Inject
+ protected BitstreamService bitstreamService;
- private static final ConfigurationService configurationService
- = DSpaceServicesFactory.getInstance().getConfigurationService();
+ @Inject
+ protected ConfigurationService configurationService;
- private static final HandleService handleService
- = HandleServiceFactory.getInstance().getHandleService();
+ @Inject
+ protected HandleService handleService;
- private static final RequestItemService requestItemService
- = RequestItemServiceFactory.getInstance().getRequestItemService();
+ @Inject
+ protected RequestItemService requestItemService;
- private static final RequestItemAuthorExtractor requestItemAuthorExtractor
- = DSpaceServicesFactory.getInstance()
- .getServiceManager()
- .getServiceByName(null, RequestItemAuthorExtractor.class);
+ protected final RequestItemAuthorExtractor requestItemAuthorExtractor;
- private RequestItemEmailNotifier() {}
+ @Inject
+ public RequestItemEmailNotifier(RequestItemAuthorExtractor requestItemAuthorExtractor) {
+ this.requestItemAuthorExtractor = requestItemAuthorExtractor;
+ }
/**
* Send the request to the approver(s).
@@ -69,31 +74,51 @@ private RequestItemEmailNotifier() {}
* @throws IOException passed through.
* @throws SQLException if the message was not sent.
*/
- static public void sendRequest(Context context, RequestItem ri, String responseLink)
+ public void sendRequest(Context context, RequestItem ri, String responseLink)
throws IOException, SQLException {
// Who is making this request?
- RequestItemAuthor author = requestItemAuthorExtractor
+ List authors = requestItemAuthorExtractor
.getRequestItemAuthor(context, ri.getItem());
- String authorEmail = author.getEmail();
- String authorName = author.getFullName();
// Build an email to the approver.
Email email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
"request_item.author"));
- email.addRecipient(authorEmail);
+ for (RequestItemAuthor author : authors) {
+ email.addRecipient(author.getEmail());
+ }
email.setReplyTo(ri.getReqEmail()); // Requester's address
+
email.addArgument(ri.getReqName()); // {0} Requester's name
+
email.addArgument(ri.getReqEmail()); // {1} Requester's address
+
email.addArgument(ri.isAllfiles() // {2} All bitstreams or just one?
? I18nUtil.getMessage("itemRequest.all") : ri.getBitstream().getName());
- email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle()));
+
+ email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {3}
+
email.addArgument(ri.getItem().getName()); // {4} requested item's title
+
email.addArgument(ri.getReqMessage()); // {5} message from requester
+
email.addArgument(responseLink); // {6} Link back to DSpace for action
- email.addArgument(authorName); // {7} corresponding author name
- email.addArgument(authorEmail); // {8} corresponding author email
- email.addArgument(configurationService.getProperty("dspace.name"));
- email.addArgument(configurationService.getProperty("mail.helpdesk"));
+
+ StringBuilder names = new StringBuilder();
+ StringBuilder addresses = new StringBuilder();
+ for (RequestItemAuthor author : authors) {
+ if (names.length() > 0) {
+ names.append("; ");
+ addresses.append("; ");
+ }
+ names.append(author.getFullName());
+ addresses.append(author.getEmail());
+ }
+ email.addArgument(names.toString()); // {7} corresponding author name
+ email.addArgument(addresses.toString()); // {8} corresponding author email
+
+ email.addArgument(configurationService.getProperty("dspace.name")); // {9}
+
+ email.addArgument(configurationService.getProperty("mail.helpdesk")); // {10}
// Send the email.
try {
@@ -126,17 +151,43 @@ static public void sendRequest(Context context, RequestItem ri, String responseL
* @param message email body (may be empty).
* @throws IOException if sending failed.
*/
- static public void sendResponse(Context context, RequestItem ri, String subject,
+ public void sendResponse(Context context, RequestItem ri, String subject,
String message)
throws IOException {
+ // Who granted this request?
+ List grantors;
+ try {
+ grantors = requestItemAuthorExtractor.getRequestItemAuthor(context, ri.getItem());
+ } catch (SQLException e) {
+ LOG.warn("Failed to get grantor's name and address: {}", e.getMessage());
+ grantors = List.of();
+ }
+
+ String grantorName;
+ String grantorAddress;
+ if (grantors.isEmpty()) {
+ grantorName = configurationService.getProperty("mail.admin.name");
+ grantorAddress = configurationService.getProperty("mail.admin");
+ } else {
+ RequestItemAuthor grantor = grantors.get(0); // XXX Cannot know which one
+ grantorName = grantor.getFullName();
+ grantorAddress = grantor.getEmail();
+ }
+
// Build an email back to the requester.
- Email email = new Email();
- email.setContent("body", message);
+ Email email = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
+ ri.isAccept_request() ? "request_item.granted" : "request_item.rejected"));
+ email.addArgument(ri.getReqName()); // {0} requestor's name
+ email.addArgument(handleService.getCanonicalForm(ri.getItem().getHandle())); // {1} URL of the requested Item
+ email.addArgument(ri.getItem().getName()); // {2} title of the requested Item
+ email.addArgument(grantorName); // {3} name of the grantor
+ email.addArgument(grantorAddress); // {4} email of the grantor
+ email.addArgument(message); // {5} grantor's optional message
email.setSubject(subject);
email.addRecipient(ri.getReqEmail());
- if (ri.isAccept_request()) {
- // Attach bitstreams.
- try {
+ // Attach bitstreams.
+ try {
+ if (ri.isAccept_request()) {
if (ri.isAllfiles()) {
Item item = ri.getItem();
List bundles = item.getBundles("ORIGINAL");
@@ -146,24 +197,40 @@ static public void sendResponse(Context context, RequestItem ri, String subject,
if (!bitstream.getFormat(context).isInternal() &&
requestItemService.isRestricted(context,
bitstream)) {
- email.addAttachment(bitstreamService.retrieve(context,
- bitstream), bitstream.getName(),
+ // #8636 Anyone receiving the email can respond to the
+ // request without authenticating into DSpace
+ context.turnOffAuthorisationSystem();
+ email.addAttachment(
+ bitstreamService.retrieve(context, bitstream),
+ bitstream.getName(),
bitstream.getFormat(context).getMIMEType());
+ context.restoreAuthSystemState();
}
}
}
} else {
Bitstream bitstream = ri.getBitstream();
+ // #8636 Anyone receiving the email can respond to the request without authenticating into DSpace
+ context.turnOffAuthorisationSystem();
email.addAttachment(bitstreamService.retrieve(context, bitstream),
bitstream.getName(),
bitstream.getFormat(context).getMIMEType());
+ context.restoreAuthSystemState();
}
email.send();
- } catch (MessagingException | IOException | SQLException | AuthorizeException e) {
- LOG.warn(LogHelper.getHeader(context,
- "error_mailing_requestItem", e.getMessage()));
- throw new IOException("Reply not sent: " + e.getMessage());
+ } else {
+ boolean sendRejectEmail = configurationService
+ .getBooleanProperty("request.item.reject.email", true);
+ // Not all sites want the "refusal" to be sent back to the requester via
+ // email. However, by default, the rejection email is sent back.
+ if (sendRejectEmail) {
+ email.send();
+ }
}
+ } catch (MessagingException | IOException | SQLException | AuthorizeException e) {
+ LOG.warn(LogHelper.getHeader(context,
+ "error_mailing_requestItem", e.getMessage()));
+ throw new IOException("Reply not sent: " + e.getMessage());
}
LOG.info(LogHelper.getHeader(context,
"sent_attach_requestItem", "token={}"), ri.getToken());
@@ -178,7 +245,7 @@ static public void sendResponse(Context context, RequestItem ri, String subject,
* @throws IOException if the message body cannot be loaded or the message
* cannot be sent.
*/
- static public void requestOpenAccess(Context context, RequestItem ri)
+ public void requestOpenAccess(Context context, RequestItem ri)
throws IOException {
Email message = Email.getEmail(I18nUtil.getEmailFilename(context.getCurrentLocale(),
"request_item.admin"));
@@ -200,8 +267,13 @@ static public void requestOpenAccess(Context context, RequestItem ri)
message.addArgument(bitstreamName); // {0} bitstream name or "all"
message.addArgument(item.getHandle()); // {1} Item handle
message.addArgument(ri.getToken()); // {2} Request token
- message.addArgument(approver.getFullName()); // {3} Approver's name
- message.addArgument(approver.getEmail()); // {4} Approver's address
+ if (approver != null) {
+ message.addArgument(approver.getFullName()); // {3} Approver's name
+ message.addArgument(approver.getEmail()); // {4} Approver's address
+ } else {
+ message.addArgument("anonymous approver"); // [3] Approver's name
+ message.addArgument(configurationService.getProperty("mail.admin")); // [4] Approver's address
+ }
// Who gets this message?
String recipient;
diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java
index 7b63d3ea8dae..dee0ed7a2351 100644
--- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java
+++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemHelpdeskStrategy.java
@@ -8,6 +8,8 @@
package org.dspace.app.requestitem;
import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Item;
@@ -16,36 +18,47 @@
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.services.ConfigurationService;
-import org.dspace.services.factory.DSpaceServicesFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.NonNull;
/**
- * RequestItem strategy to allow DSpace support team's helpdesk to receive requestItem request
- * With this enabled, then the Item author/submitter doesn't receive the request, but the helpdesk instead does.
+ * RequestItem strategy to allow DSpace support team's help desk to receive
+ * requestItem requests. With this enabled, the Item author/submitter doesn't
+ * receive the request, but the help desk instead does.
*
- * Failover to the RequestItemSubmitterStrategy, which means the submitter would get the request if there is no
- * specified helpdesk email.
+ *