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 0000000000..ee6b8d08b0 --- /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 0000000000..292c6c372e --- /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 0000000000..043990156d --- /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 0000000000..8d189038d9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java @@ -0,0 +1,70 @@ +/** + * 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.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 ProcessCleaner} script. + */ +public class ProcessCleanerConfiguration 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) { + + 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/app/requestitem/CollectionAdministratorsRequestItemStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategy.java new file mode 100644 index 0000000000..135406069a --- /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 0000000000..8292c1a728 --- /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 9e675e97a7..cdefd1298c 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.Context; import org.dspace.core.ReloadableEntity; /** - * Object representing an Item Request + * Object representing an Item Request. */ @Entity @Table(name = "requestitem") @@ -94,6 +94,9 @@ public class RequestItem implements ReloadableEntity { this.allfiles = allfiles; } + /** + * @return {@code true} if all of the Item's files are requested. + */ public boolean isAllfiles() { return allfiles; } @@ -102,6 +105,9 @@ public class RequestItem implements ReloadableEntity { this.reqMessage = reqMessage; } + /** + * @return a message from the requester. + */ public String getReqMessage() { return reqMessage; } @@ -110,6 +116,9 @@ public class RequestItem implements ReloadableEntity { this.reqName = reqName; } + /** + * @return Human-readable name of the user requesting access. + */ public String getReqName() { return reqName; } @@ -118,6 +127,9 @@ public class RequestItem implements ReloadableEntity { this.reqEmail = reqEmail; } + /** + * @return address of the user requesting access. + */ public String getReqEmail() { return reqEmail; } @@ -126,6 +138,9 @@ public class RequestItem implements ReloadableEntity { 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 49e26fe00b..a189e4a5ef 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 @@ import org.dspace.eperson.EPerson; /** * 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 9b66030e90..bc97bc64bf 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 d72e42eac1..02054ee1a0 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 @@ -72,28 +72,48 @@ public class RequestItemEmailNotifier { static 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 { 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 7b63d3ea8d..f440ba380a 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,11 +18,11 @@ import org.dspace.core.I18nUtil; 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 + * 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. * * Failover to the RequestItemSubmitterStrategy, which means the submitter would get the request if there is no @@ -33,19 +35,24 @@ public class RequestItemHelpdeskStrategy extends RequestItemSubmitterStrategy { @Autowired(required = true) protected EPersonService ePersonService; + @Autowired(required = true) + private ConfigurationService configuration; + public RequestItemHelpdeskStrategy() { } @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) throws SQLException { - ConfigurationService configurationService - = DSpaceServicesFactory.getInstance().getConfigurationService(); - boolean helpdeskOverridesSubmitter = configurationService + @NonNull + public List getRequestItemAuthor(Context context, Item item) + throws SQLException { + boolean helpdeskOverridesSubmitter = configuration .getBooleanProperty("request.item.helpdesk.override", false); - String helpDeskEmail = configurationService.getProperty("mail.helpdesk"); + String helpDeskEmail = configuration.getProperty("mail.helpdesk"); if (helpdeskOverridesSubmitter && StringUtils.isNotBlank(helpDeskEmail)) { - return getHelpDeskPerson(context, helpDeskEmail); + List authors = new ArrayList<>(1); + authors.add(getHelpDeskPerson(context, helpDeskEmail)); + return authors; } else { //Fallback to default logic (author of Item) if helpdesk isn't fully enabled or setup return super.getRequestItemAuthor(context, item); diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java index 1737490fbb..4372ab9b09 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemMetadataStrategy.java @@ -8,6 +8,8 @@ package org.dspace.app.requestitem; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.commons.lang3.StringUtils; @@ -16,12 +18,13 @@ import org.dspace.content.MetadataValue; import org.dspace.content.service.ItemService; import org.dspace.core.Context; import org.dspace.core.I18nUtil; -import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; /** * Try to look to an item metadata for the corresponding author name and email. - * Failover to the RequestItemSubmitterStrategy + * Failover to the RequestItemSubmitterStrategy. * * @author Andrea Bollini */ @@ -30,6 +33,9 @@ public class RequestItemMetadataStrategy extends RequestItemSubmitterStrategy { protected String emailMetadata; protected String fullNameMetadata; + @Autowired(required = true) + protected ConfigurationService configurationService; + @Autowired(required = true) protected ItemService itemService; @@ -37,59 +43,72 @@ public class RequestItemMetadataStrategy extends RequestItemSubmitterStrategy { } @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) + @NonNull + public List getRequestItemAuthor(Context context, Item item) throws SQLException { - RequestItemAuthor author = null; + List authors; if (emailMetadata != null) { List vals = itemService.getMetadataByMetadataString(item, emailMetadata); - if (vals.size() > 0) { - String email = vals.iterator().next().getValue(); - String fullname = null; - if (fullNameMetadata != null) { - List nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); - if (nameVals.size() > 0) { - fullname = nameVals.iterator().next().getValue(); + List nameVals; + if (null != fullNameMetadata) { + nameVals = itemService.getMetadataByMetadataString(item, fullNameMetadata); + } else { + nameVals = Collections.EMPTY_LIST; + } + boolean useNames = vals.size() == nameVals.size(); + if (!vals.isEmpty()) { + authors = new ArrayList<>(vals.size()); + for (int authorIndex = 0; authorIndex < vals.size(); authorIndex++) { + String email = vals.get(authorIndex).getValue(); + String fullname = null; + if (useNames) { + fullname = nameVals.get(authorIndex).getValue(); } + + if (StringUtils.isBlank(fullname)) { + fullname = I18nUtil.getMessage( + "org.dspace.app.requestitem.RequestItemMetadataStrategy.unnamed", + context); + } + RequestItemAuthor author = new RequestItemAuthor( + fullname, email); + authors.add(author); } - if (StringUtils.isBlank(fullname)) { - fullname = I18nUtil - .getMessage( - "org.dspace.app.requestitem.RequestItemMetadataStrategy.unnamed", - context); - } - author = new RequestItemAuthor(fullname, email); - return author; + return authors; + } else { + return Collections.EMPTY_LIST; } } else { // Uses the basic strategy to look for the original submitter - author = super.getRequestItemAuthor(context, item); - // Is the author or their email null. If so get the help desk or admin name and email - if (null == author || null == author.getEmail()) { - String email = null; - String name = null; + authors = super.getRequestItemAuthor(context, item); + + // Remove from the list authors that do not have email addresses. + for (RequestItemAuthor author : authors) { + if (null == author.getEmail()) { + authors.remove(author); + } + } + + if (authors.isEmpty()) { // No author email addresses! Fall back //First get help desk name and email - email = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("mail.helpdesk"); - name = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("mail.helpdesk.name"); + String email = configurationService.getProperty("mail.helpdesk"); + String name = configurationService.getProperty("mail.helpdesk.name"); // If help desk mail is null get the mail and name of admin if (email == null) { - email = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("mail.admin"); - name = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("mail.admin.name"); + email = configurationService.getProperty("mail.admin"); + name = configurationService.getProperty("mail.admin.name"); } - author = new RequestItemAuthor(name, email); + authors.add(new RequestItemAuthor(name, email)); } + return authors; } - return author; } - public void setEmailMetadata(String emailMetadata) { + public void setEmailMetadata(@NonNull String emailMetadata) { this.emailMetadata = emailMetadata; } - public void setFullNameMetadata(String fullNameMetadata) { + public void setFullNameMetadata(@NonNull String fullNameMetadata) { this.fullNameMetadata = fullNameMetadata; } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java index 2708c24ba9..dcc1a3e80e 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemSubmitterStrategy.java @@ -8,10 +8,13 @@ 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.dspace.eperson.EPerson; +import org.springframework.lang.NonNull; /** * Basic strategy that looks to the original submitter. @@ -24,21 +27,23 @@ public class RequestItemSubmitterStrategy implements RequestItemAuthorExtractor } /** - * Returns the submitter of an Item as RequestItemAuthor or null if the - * Submitter is deleted. + * Returns the submitter of an Item as RequestItemAuthor or an empty List if + * the Submitter is deleted. * - * @return The submitter of the item or null if the submitter is deleted + * @return The submitter of the item or empty List if the submitter is deleted * @throws SQLException if database error */ @Override - public RequestItemAuthor getRequestItemAuthor(Context context, Item item) + @NonNull + public List getRequestItemAuthor(Context context, Item item) throws SQLException { EPerson submitter = item.getSubmitter(); - RequestItemAuthor author = null; + List authors = new ArrayList<>(1); if (null != submitter) { - author = new RequestItemAuthor( - submitter.getFullName(), submitter.getEmail()); + RequestItemAuthor author = new RequestItemAuthor( + submitter.getFullName(), submitter.getEmail()); + authors.add(author); } - return author; + return authors; } } diff --git a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java index 89ca477442..514143c93e 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java @@ -20,6 +20,7 @@ import com.rometools.modules.opensearch.OpenSearchModule; import com.rometools.modules.opensearch.entity.OSQuery; import com.rometools.modules.opensearch.impl.OpenSearchModuleImpl; import com.rometools.rome.io.FeedException; +import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.util.service.OpenSearchService; import org.dspace.content.DSpaceObject; @@ -96,7 +97,7 @@ public class OpenSearchServiceImpl implements OpenSearchService { * Get base search UI URL (websvc.opensearch.uicontext) */ protected String getBaseSearchUIURL() { - return configurationService.getProperty("dspace.server.url") + "/" + + return configurationService.getProperty("dspace.ui.url") + "/" + configurationService.getProperty("websvc.opensearch.uicontext"); } @@ -177,7 +178,9 @@ public class OpenSearchServiceImpl implements OpenSearchService { OSQuery osq = new OSQuery(); osq.setRole("request"); try { - osq.setSearchTerms(URLEncoder.encode(query, "UTF-8")); + if (StringUtils.isNotBlank(query)) { + osq.setSearchTerms(URLEncoder.encode(query, "UTF-8")); + } } catch (UnsupportedEncodingException e) { log.error(e); } diff --git a/dspace-api/src/main/java/org/dspace/authorize/RegexPasswordValidator.java b/dspace-api/src/main/java/org/dspace/authorize/RegexPasswordValidator.java new file mode 100644 index 0000000000..d12c3ba919 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/RegexPasswordValidator.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.authorize; + +import static org.apache.commons.lang.StringUtils.isNotBlank; + +import java.util.regex.Pattern; + +import org.dspace.authorize.service.PasswordValidatorService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link PasswordValidatorService} that verifies if the given + * passowrd matches the configured pattern. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +public class RegexPasswordValidator implements PasswordValidatorService { + + @Autowired + private ConfigurationService configurationService; + + @Override + public boolean isPasswordValidationEnabled() { + return isNotBlank(getPasswordValidationPattern()); + } + + @Override + public boolean isPasswordValid(String password) { + if (!isPasswordValidationEnabled()) { + return true; + } + + Pattern pattern = Pattern.compile(getPasswordValidationPattern()); + return pattern.matcher(password).find(); + } + + private String getPasswordValidationPattern() { + return configurationService.getProperty("authentication-password.regex-validation.pattern"); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/ValidatePasswordServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/ValidatePasswordServiceImpl.java new file mode 100644 index 0000000000..663308d627 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/ValidatePasswordServiceImpl.java @@ -0,0 +1,33 @@ +/** + * 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.authorize; + +import java.util.List; + +import org.dspace.authorize.service.PasswordValidatorService; +import org.dspace.authorize.service.ValidatePasswordService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Basic implementation for validation password robustness. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class ValidatePasswordServiceImpl implements ValidatePasswordService { + + @Autowired + private List validators; + + @Override + public boolean isPasswordValid(String password) { + return validators.stream() + .filter(passwordValidator -> passwordValidator.isPasswordValidationEnabled()) + .allMatch(passwordValidator -> passwordValidator.isPasswordValid(password)); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/PasswordValidatorService.java b/dspace-api/src/main/java/org/dspace/authorize/service/PasswordValidatorService.java new file mode 100644 index 0000000000..5817969b6d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/service/PasswordValidatorService.java @@ -0,0 +1,29 @@ +/** + * 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.authorize.service; + +/** + * Interface for classes that validate a given password with a specific + * strategy. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +public interface PasswordValidatorService { + + /** + * Check if the password validator is active. + */ + public boolean isPasswordValidationEnabled(); + + /** + * This method checks whether the password is valid + * + * @param password password to validate + */ + public boolean isPasswordValid(String password); +} diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/ValidatePasswordService.java b/dspace-api/src/main/java/org/dspace/authorize/service/ValidatePasswordService.java new file mode 100644 index 0000000000..0d5f6191f6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/authorize/service/ValidatePasswordService.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.authorize.service; + +/** + * Services to use during Validating of password. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public interface ValidatePasswordService { + + /** + * This method checks whether the password is valid based on the configured + * rules/strategies. + * + * @param password password to validate + */ + public boolean isPasswordValid(String password); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java index 4ef26cffcb..69bac319c6 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java @@ -8,8 +8,10 @@ package org.dspace.content.dao; import java.sql.SQLException; +import java.util.Date; import java.util.List; +import org.dspace.content.ProcessStatus; import org.dspace.core.Context; import org.dspace.core.GenericDAO; import org.dspace.scripts.Process; @@ -81,4 +83,18 @@ public interface ProcessDAO extends GenericDAO { int countTotalWithParameters(Context context, ProcessQueryParameterContainer processQueryParameterContainer) throws SQLException; + + /** + * Find all the processes with one of the given status and with a creation time + * older than the specified date. + * + * @param context The relevant DSpace context + * @param statuses the statuses of the processes to search for + * @param date the creation date to search for + * @return The list of all Processes which match requirements + * @throws SQLException If something goes wrong + */ + List findByStatusAndCreationTimeOlderThan(Context context, List statuses, Date date) + throws SQLException; + } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java index 5c8083a86b..23ce6ce381 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java @@ -7,7 +7,10 @@ */ package org.dspace.content.dao.impl; +import static org.dspace.scripts.Process_.CREATION_TIME; + import java.sql.SQLException; +import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -17,6 +20,7 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import org.apache.commons.lang3.StringUtils; +import org.dspace.content.ProcessStatus; import org.dspace.content.dao.ProcessDAO; import org.dspace.core.AbstractHibernateDAO; import org.dspace.core.Context; @@ -147,6 +151,23 @@ public class ProcessDAOImpl extends AbstractHibernateDAO implements Pro } + @Override + public List findByStatusAndCreationTimeOlderThan(Context context, List statuses, + Date date) throws SQLException { + + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Process.class); + + Root processRoot = criteriaQuery.from(Process.class); + criteriaQuery.select(processRoot); + + Predicate creationTimeLessThanGivenDate = criteriaBuilder.lessThan(processRoot.get(CREATION_TIME), date); + Predicate statusIn = processRoot.get(Process_.PROCESS_STATUS).in(statuses); + criteriaQuery.where(criteriaBuilder.and(creationTimeLessThanGivenDate, statusIn)); + + return list(context, criteriaQuery, false, Process.class, -1, -1); + } + } diff --git a/dspace-api/src/main/java/org/dspace/curate/CitationPage.java b/dspace-api/src/main/java/org/dspace/ctask/general/CitationPage.java similarity index 83% rename from dspace-api/src/main/java/org/dspace/curate/CitationPage.java rename to dspace-api/src/main/java/org/dspace/ctask/general/CitationPage.java index cbd501f103..fa630029b8 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CitationPage.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/CitationPage.java @@ -5,7 +5,7 @@ * * http://www.dspace.org/license/ */ -package org.dspace.curate; +package org.dspace.ctask.general; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -18,6 +18,9 @@ import java.util.Map; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; @@ -26,6 +29,10 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BundleService; import org.dspace.core.Context; +import org.dspace.curate.AbstractCurationTask; +import org.dspace.curate.Curator; +import org.dspace.curate.Distributive; +import org.dspace.curate.Mutative; import org.dspace.disseminate.factory.DisseminateServiceFactory; import org.dspace.disseminate.service.CitationDocumentService; @@ -67,6 +74,10 @@ public class CitationPage extends AbstractCurationTask { protected BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService(); + protected ResourcePolicyService resourcePolicyService = AuthorizeServiceFactory.getInstance() + .getResourcePolicyService(); + + private Map displayMap = new HashMap(); /** * {@inheritDoc} @@ -95,10 +106,13 @@ public class CitationPage extends AbstractCurationTask { protected void performItem(Item item) throws SQLException { //Determine if the DISPLAY bundle exits. If not, create it. List dBundles = itemService.getBundles(item, CitationPage.DISPLAY_BUNDLE_NAME); + Bundle original = itemService.getBundles(item, "ORIGINAL").get(0); Bundle dBundle = null; if (dBundles == null || dBundles.isEmpty()) { try { dBundle = bundleService.create(Curator.curationContext(), item, CitationPage.DISPLAY_BUNDLE_NAME); + // don't inherit now otherwise they will be copied over the moved bitstreams + resourcePolicyService.removeAllPolicies(Curator.curationContext(), dBundle); } catch (AuthorizeException e) { log.error("User not authroized to create bundle on item \"{}\": {}", item::getName, e::getMessage); @@ -110,7 +124,6 @@ public class CitationPage extends AbstractCurationTask { //Create a map of the bitstreams in the displayBundle. This is used to //check if the bundle being cited is already in the display bundle. - Map displayMap = new HashMap<>(); for (Bitstream bs : dBundle.getBitstreams()) { displayMap.put(bs.getName(), bs); } @@ -128,6 +141,8 @@ public class CitationPage extends AbstractCurationTask { } else { try { pBundle = bundleService.create(Curator.curationContext(), item, CitationPage.PRESERVATION_BUNDLE_NAME); + // don't inherit now otherwise they will be copied over the moved bitstreams + resourcePolicyService.removeAllPolicies(Curator.curationContext(), pBundle); } catch (AuthorizeException e) { log.error("User not authroized to create bundle on item \"" + item.getName() + "\": " + e.getMessage()); @@ -160,7 +175,10 @@ public class CitationPage extends AbstractCurationTask { citationDocument.makeCitedDocument(Curator.curationContext(), bitstream).getLeft()); //Add the cited document to the approiate bundle this.addCitedPageToItem(citedInputStream, bundle, pBundle, - dBundle, displayMap, item, bitstream); + dBundle, item, bitstream); + // now set the policies of the preservation and display bundle + clonePolicies(Curator.curationContext(), original, pBundle); + clonePolicies(Curator.curationContext(), original, dBundle); } catch (Exception e) { //Could be many things, but nothing that should be //expected. @@ -203,8 +221,6 @@ public class CitationPage extends AbstractCurationTask { * @param pBundle The preservation bundle. The original document should be * put in here if it is not already. * @param dBundle The display bundle. The cited document gets put in here. - * @param displayMap The map of bitstream names to bitstreams in the display - * bundle. * @param item The item containing the bundles being used. * @param bitstream The original source bitstream. * @throws SQLException if database error @@ -212,7 +228,7 @@ public class CitationPage extends AbstractCurationTask { * @throws IOException if IO error */ protected void addCitedPageToItem(InputStream citedDoc, Bundle bundle, Bundle pBundle, - Bundle dBundle, Map displayMap, Item item, + Bundle dBundle, Item item, Bitstream bitstream) throws SQLException, AuthorizeException, IOException { //If we are modifying a file that is not in the //preservation bundle then we have to move it there. @@ -240,7 +256,8 @@ public class CitationPage extends AbstractCurationTask { citedBitstream.setName(context, bitstream.getName()); bitstreamService.setFormat(context, citedBitstream, bitstream.getFormat(Curator.curationContext())); citedBitstream.setDescription(context, bitstream.getDescription()); - + displayMap.put(bitstream.getName(), citedBitstream); + clonePolicies(context, bitstream, citedBitstream); this.resBuilder.append(" Added ") .append(citedBitstream.getName()) .append(" to the ") @@ -252,4 +269,16 @@ public class CitationPage extends AbstractCurationTask { itemService.update(context, item); this.status = Curator.CURATE_SUCCESS; } + + private void clonePolicies(Context context, DSpaceObject source,DSpaceObject target) + throws SQLException, AuthorizeException { + resourcePolicyService.removeAllPolicies(context, target); + for (ResourcePolicy rp: source.getResourcePolicies()) { + ResourcePolicy newPolicy = resourcePolicyService.clone(context, rp); + newPolicy.setdSpaceObject(target); + newPolicy.setAction(rp.getAction()); + resourcePolicyService.update(context, newPolicy); + } + + } } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java index 3dcd7d16a6..8ca5b7c0ea 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -33,6 +34,7 @@ import org.dspace.content.dto.MetadataValueDTO; import org.dspace.external.OpenAIRERestConnector; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; import org.springframework.beans.factory.annotation.Autowired; /** @@ -40,13 +42,9 @@ import org.springframework.beans.factory.annotation.Autowired; * will deal with the OpenAIRE External Data lookup * * @author paulo-graca - * */ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { - /** - * log4j logger - */ private static Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenAIREFundingDataProvider.class); /** @@ -54,6 +52,16 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { */ protected static final String PREFIX = "info:eu-repo/grantAgreement"; + private static final String TITLE = "dcTitle"; + private static final String SUBJECT = "dcSubject"; + private static final String AWARD_URI = "awardURI"; + private static final String FUNDER_NAME = "funderName"; + private static final String SPATIAL = "coverageSpatial"; + private static final String AWARD_NUMBER = "awardNumber"; + private static final String FUNDER_ID = "funderIdentifier"; + private static final String FUNDING_STREAM = "fundingStream"; + private static final String TITLE_ALTERNATIVE = "titleAlternative"; + /** * rows default limit */ @@ -69,11 +77,9 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { */ protected OpenAIRERestConnector connector; - /** - * required method - */ - public void init() throws IOException { - } + protected Map metadataFields; + + public void init() throws IOException {} @Override public String getSourceIdentifier() { @@ -266,14 +272,22 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { } } + public Map getMetadataFields() { + return metadataFields; + } + + public void setMetadataFields(Map metadataFields) { + this.metadataFields = metadataFields; + } + /** * OpenAIRE Funding External Data Builder Class * * @author pgraca - * */ - public static class ExternalDataObjectBuilder { - ExternalDataObject externalDataObject; + public class ExternalDataObjectBuilder { + + private ExternalDataObject externalDataObject; public ExternalDataObjectBuilder(Project project) { String funderIdPrefix = "urn:openaire:"; @@ -283,46 +297,42 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { for (FundingTreeType fundingTree : projectHelper.getFundingTreeTypes()) { FunderType funder = fundingTree.getFunder(); // Funder name - this.addFunderName(funder.getName()); + this.addMetadata(metadataFields.get(FUNDER_NAME), funder.getName()); // Funder Id - convert it to an urn - this.addFunderID(funderIdPrefix + funder.getId()); + this.addMetadata(metadataFields.get(FUNDER_ID), funderIdPrefix + funder.getId()); // Jurisdiction - this.addFunderJuristiction(funder.getJurisdiction()); + this.addMetadata(metadataFields.get(SPATIAL), funder.getJurisdiction()); FundingHelper fundingHelper = new FundingHelper( - fundingTree.getFundingLevel2OrFundingLevel1OrFundingLevel0()); + fundingTree.getFundingLevel2OrFundingLevel1OrFundingLevel0()); // Funding description for (FundingType funding : fundingHelper.getFirstAvailableFunding()) { - this.addFundingStream(funding.getDescription()); + this.addMetadata(metadataFields.get(FUNDING_STREAM), funding.getDescription()); } } // Title for (String title : projectHelper.getTitles()) { - this.addAwardTitle(title); + this.addMetadata(metadataFields.get(TITLE), title); this.setDisplayValue(title); this.setValue(title); } - // Code for (String code : projectHelper.getCodes()) { - this.addAwardNumber(code); + this.addMetadata(metadataFields.get(AWARD_NUMBER), code); } - // Website url for (String url : projectHelper.getWebsiteUrls()) { - this.addAwardURI(url); + this.addMetadata(metadataFields.get(AWARD_URI), url); } - // Acronyms for (String acronym : projectHelper.getAcronyms()) { - this.addFundingItemAcronym(acronym); + this.addMetadata(metadataFields.get(TITLE_ALTERNATIVE), acronym); } - // Keywords for (String keyword : projectHelper.getKeywords()) { - this.addSubject(keyword); + this.addMetadata(metadataFields.get(SUBJECT), keyword); } } @@ -366,7 +376,6 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { * @return ExternalDataObjectBuilder */ public ExternalDataObjectBuilder setId(String id) { - // we use base64 encoding in order to use slashes / and other // characters that must be escaped for the <:entry-id> String base64Id = Base64.getEncoder().encodeToString(id.getBytes()); @@ -374,128 +383,10 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { return this; } - /** - * Add metadata dc.identifier - * - * @param metadata identifier - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addIdentifier(String identifier) { - this.externalDataObject.addMetadata(new MetadataValueDTO("dc", "identifier", null, null, identifier)); - return this; - } - - /** - * Add metadata project.funder.name - * - * @param metadata funderName - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addFunderName(String funderName) { - this.externalDataObject.addMetadata(new MetadataValueDTO("project", "funder", "name", null, funderName)); - return this; - } - - /** - * Add metadata project.funder.identifier - * - * @param metadata funderId - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addFunderID(String funderID) { - this.externalDataObject - .addMetadata(new MetadataValueDTO("project", "funder", "identifier", null, funderID)); - return this; - } - - /** - * Add metadata dc.title - * - * @param metadata awardTitle - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addAwardTitle(String awardTitle) { - this.externalDataObject.addMetadata(new MetadataValueDTO("dc", "title", null, null, awardTitle)); - return this; - } - - /** - * Add metadata oaire.fundingStream - * - * @param metadata fundingStream - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addFundingStream(String fundingStream) { - this.externalDataObject - .addMetadata(new MetadataValueDTO("oaire", "fundingStream", null, null, fundingStream)); - return this; - } - - /** - * Add metadata oaire.awardNumber - * - * @param metadata awardNumber - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addAwardNumber(String awardNumber) { - this.externalDataObject.addMetadata(new MetadataValueDTO("oaire", "awardNumber", null, null, awardNumber)); - return this; - } - - /** - * Add metadata oaire.awardURI - * - * @param metadata websiteUrl - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addAwardURI(String websiteUrl) { - this.externalDataObject.addMetadata(new MetadataValueDTO("oaire", "awardURI", null, null, websiteUrl)); - return this; - } - - /** - * Add metadata dc.title.alternative - * - * @param metadata fundingItemAcronym - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addFundingItemAcronym(String fundingItemAcronym) { - this.externalDataObject - .addMetadata(new MetadataValueDTO("dc", "title", "alternative", null, fundingItemAcronym)); - return this; - } - - /** - * Add metadata dc.coverage.spatial - * - * @param metadata funderJuristiction - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addFunderJuristiction(String funderJuristiction) { - this.externalDataObject - .addMetadata(new MetadataValueDTO("dc", "coverage", "spatial", null, funderJuristiction)); - return this; - } - - /** - * Add metadata dc.description - * - * @param metadata description - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addDescription(String description) { - this.externalDataObject.addMetadata(new MetadataValueDTO("dc", "description", null, null, description)); - return this; - } - - /** - * Add metadata dc.subject - * - * @param metadata subject - * @return ExternalDataObjectBuilder - */ - public ExternalDataObjectBuilder addSubject(String subject) { - this.externalDataObject.addMetadata(new MetadataValueDTO("dc", "subject", null, null, subject)); + public ExternalDataObjectBuilder addMetadata(MetadataFieldConfig metadataField, String value) { + this.externalDataObject.addMetadata(new MetadataValueDTO(metadataField.getSchema(), + metadataField.getElement(), + metadataField.getQualifier(), null, value)); return this; } @@ -508,4 +399,5 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider { return this.externalDataObject; } } -} + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SplitMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SplitMetadataContributor.java new file mode 100644 index 0000000000..c04081957f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SplitMetadataContributor.java @@ -0,0 +1,65 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; + +import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * Wrapper class used to split another MetadataContributor's output into distinct values. + * The split is performed by matching a regular expression against the wrapped MetadataContributor's output. + * + * @author Philipp Rumpf (philipp.rumpf@uni-bamberg.de) + */ + +public class SplitMetadataContributor implements MetadataContributor { + private final MetadataContributor innerContributor; + private final String regex; + + /** + * @param innerContributor The MetadataContributor whose output is split + * @param regex A regular expression matching the separator between different values + */ + public SplitMetadataContributor(MetadataContributor innerContributor, String regex) { + this.innerContributor = innerContributor; + this.regex = regex; + } + + @Override + public void setMetadataFieldMapping(MetadataFieldMapping> rt) { + + } + + /** + * Each metadatum returned by the wrapped MetadataContributor is split into one or more metadata values + * based on the provided regular expression. + * + * @param t The recordType object to retrieve metadata from + * @return The collection of processed metadata values + */ + @Override + public Collection contributeMetadata(T t) { + Collection metadata = innerContributor.contributeMetadata(t); + ArrayList splitMetadata = new ArrayList<>(); + for (MetadatumDTO metadatumDTO : metadata) { + String[] split = metadatumDTO.getValue().split(regex); + for (String splitItem : split) { + MetadatumDTO splitMetadatumDTO = new MetadatumDTO(); + splitMetadatumDTO.setSchema(metadatumDTO.getSchema()); + splitMetadatumDTO.setElement(metadatumDTO.getElement()); + splitMetadatumDTO.setQualifier(metadatumDTO.getQualifier()); + splitMetadatumDTO.setValue(splitItem); + splitMetadata.add(splitMetadatumDTO); + } + } + return splitMetadata; + } +} diff --git a/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactory.java b/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactory.java new file mode 100644 index 0000000000..81cebb84a1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactory.java @@ -0,0 +1,29 @@ +/** + * 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.passwordvalidation.factory; + +import org.dspace.authorize.service.PasswordValidatorService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Abstract factory to get services for the passwordvalidation package, + * use PasswordValidationFactory.getInstance() to retrieve an implementation. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public abstract class PasswordValidationFactory { + + public abstract PasswordValidatorService getPasswordValidationService(); + + public static PasswordValidationFactory getInstance() { + return DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName("validationPasswordFactory", PasswordValidationFactory.class); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactoryImpl.java b/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactoryImpl.java new file mode 100644 index 0000000000..a73c7f6868 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/passwordvalidation/factory/PasswordValidationFactoryImpl.java @@ -0,0 +1,29 @@ +/** + * 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.passwordvalidation.factory; + +import org.dspace.authorize.service.PasswordValidatorService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory implementation to get services for the PasswordValidation package, + * use PasswordValidationFactory.getInstance() to retrieve an implementation. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class PasswordValidationFactoryImpl extends PasswordValidationFactory { + + @Autowired(required = true) + private PasswordValidatorService PasswordValidatorService; + + @Override + public PasswordValidatorService getPasswordValidationService() { + return PasswordValidatorService; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java index 3dc859ef47..33fea75add 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java @@ -305,6 +305,12 @@ public class ProcessServiceImpl implements ProcessService { tempFile.delete(); } + @Override + public List findByStatusAndCreationTimeOlderThan(Context context, List statuses, + Date date) throws SQLException { + return this.processDAO.findByStatusAndCreationTimeOlderThan(context, statuses, date); + } + private String formatLogLine(int processId, String scriptName, String output, ProcessLogLevel processLogLevel) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); StringBuilder sb = new StringBuilder(); diff --git a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java index 95bcdd3270..ce6a173b0e 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java +++ b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java @@ -10,11 +10,13 @@ package org.dspace.scripts.service; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.Date; import java.util.List; import java.util.Set; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; +import org.dspace.content.ProcessStatus; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -240,4 +242,17 @@ public interface ProcessService { */ void createLogBitstream(Context context, Process process) throws IOException, SQLException, AuthorizeException; + + /** + * Find all the processes with one of the given status and with a creation time + * older than the specified date. + * + * @param context The relevant DSpace context + * @param statuses the statuses of the processes to search for + * @param date the creation date to search for + * @return The list of all Processes which match requirements + * @throws AuthorizeException If something goes wrong + */ + List findByStatusAndCreationTimeOlderThan(Context context, List statuses, Date date) + throws SQLException; } diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties index b537819c06..c478e4e69b 100644 --- a/dspace-api/src/main/resources/Messages.properties +++ b/dspace-api/src/main/resources/Messages.properties @@ -120,3 +120,4 @@ org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException.message = Refused org.dspace.app.rest.exception.EPersonNameNotProvidedException.message = The eperson.firstname and eperson.lastname values need to be filled in org.dspace.app.rest.exception.GroupNameNotProvidedException.message = Cannot create group, no group name is provided org.dspace.app.rest.exception.GroupHasPendingWorkflowTasksException.message = Cannot delete group, the associated workflow role still has pending tasks +org.dspace.app.rest.exception.PasswordNotValidException.message = New password is invalid. Valid passwords must be at least 8 characters long! diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml index e10d04a16f..f1e6c30d13 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml @@ -1,6 +1,10 @@ - @@ -15,11 +19,71 @@ init-method="init"> + Project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index bae2dd11ae..eb3ee82f60 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -64,7 +64,12 @@ - + + + + + + diff --git a/dspace-api/src/test/java/org/dspace/administer/ProcessCleanerIT.java b/dspace-api/src/test/java/org/dspace/administer/ProcessCleanerIT.java new file mode 100644 index 0000000000..4676236cfe --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/administer/ProcessCleanerIT.java @@ -0,0 +1,380 @@ +/** + * 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 static org.apache.commons.lang.time.DateUtils.addDays; +import static org.dspace.content.ProcessStatus.COMPLETED; +import static org.dspace.content.ProcessStatus.FAILED; +import static org.dspace.content.ProcessStatus.RUNNING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.launcher.ScriptLauncher; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; +import org.dspace.builder.ProcessBuilder; +import org.dspace.content.ProcessStatus; +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.junit.Test; + +/** + * Integration tests for {@link ProcessCleaner}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ProcessCleanerIT extends AbstractIntegrationTestWithDatabase { + + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + private ProcessService processService = ScriptServiceFactory.getInstance().getProcessService(); + + @Test + public void testWithoutProcessToDelete() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertThat(testDSpaceRunnableHandler.getErrorMessages(), empty()); + assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED]")); + assertThat(messages, hasItem("Found 0 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + + } + + @Test + public void testWithoutSpecifiedStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertThat(testDSpaceRunnableHandler.getErrorMessages(), empty()); + assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED]")); + assertThat(messages, hasItem("Found 2 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), nullValue()); + assertThat(processService.find(context, process_5.getID()), nullValue()); + assertThat(processService.find(context, process_6.getID()), notNullValue()); + assertThat(processService.find(context, process_7.getID()), notNullValue()); + + } + + @Test + public void testWithCompletedStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-c" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertThat(testDSpaceRunnableHandler.getErrorMessages(), empty()); + assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED]")); + assertThat(messages, hasItem("Found 2 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), nullValue()); + assertThat(processService.find(context, process_5.getID()), nullValue()); + assertThat(processService.find(context, process_6.getID()), notNullValue()); + assertThat(processService.find(context, process_7.getID()), notNullValue()); + + } + + @Test + public void testWithRunningStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(RUNNING, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-r" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertThat(testDSpaceRunnableHandler.getErrorMessages(), empty()); + assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [RUNNING]")); + assertThat(messages, hasItem("Found 2 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), notNullValue()); + assertThat(processService.find(context, process_5.getID()), notNullValue()); + assertThat(processService.find(context, process_6.getID()), nullValue()); + assertThat(processService.find(context, process_7.getID()), notNullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + @Test + public void testWithFailedStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(FAILED, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-f" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertThat(testDSpaceRunnableHandler.getErrorMessages(), empty()); + assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [FAILED]")); + assertThat(messages, hasItem("Found 2 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), notNullValue()); + assertThat(processService.find(context, process_5.getID()), notNullValue()); + assertThat(processService.find(context, process_6.getID()), notNullValue()); + assertThat(processService.find(context, process_7.getID()), nullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + @Test + public void testWithCompletedAndFailedStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(FAILED, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-c", "-f" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED, FAILED]")); + assertThat(messages, hasItem("Found 4 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), nullValue()); + assertThat(processService.find(context, process_5.getID()), nullValue()); + assertThat(processService.find(context, process_6.getID()), notNullValue()); + assertThat(processService.find(context, process_7.getID()), nullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + @Test + public void testWithCompletedAndRunningStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(RUNNING, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-c", "-r" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED, RUNNING]")); + assertThat(messages, hasItem("Found 4 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), nullValue()); + assertThat(processService.find(context, process_5.getID()), nullValue()); + assertThat(processService.find(context, process_6.getID()), nullValue()); + assertThat(processService.find(context, process_7.getID()), notNullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + @Test + public void testWithFailedAndRunningStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(RUNNING, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-f", "-r" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [FAILED, RUNNING]")); + assertThat(messages, hasItem("Found 3 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), notNullValue()); + assertThat(processService.find(context, process_5.getID()), notNullValue()); + assertThat(processService.find(context, process_6.getID()), nullValue()); + assertThat(processService.find(context, process_7.getID()), nullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + @Test + public void testWithCompletedFailedAndRunningStatus() throws Exception { + + Process process_1 = buildProcess(COMPLETED, addDays(new Date(), -2)); + Process process_2 = buildProcess(RUNNING, addDays(new Date(), -1)); + Process process_3 = buildProcess(FAILED, addDays(new Date(), -3)); + Process process_4 = buildProcess(COMPLETED, addDays(new Date(), -6)); + Process process_5 = buildProcess(COMPLETED, addDays(new Date(), -8)); + Process process_6 = buildProcess(RUNNING, addDays(new Date(), -7)); + Process process_7 = buildProcess(FAILED, addDays(new Date(), -8)); + Process process_8 = buildProcess(RUNNING, addDays(new Date(), -9)); + + configurationService.setProperty("process-cleaner.days", 5); + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "process-cleaner", "-f", "-r", "-c" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + List messages = testDSpaceRunnableHandler.getInfoMessages(); + assertThat(messages, hasSize(3)); + assertThat(messages, hasItem("Searching for processes with status: [COMPLETED, FAILED, RUNNING]")); + assertThat(messages, hasItem("Found 5 processes to be deleted")); + assertThat(messages, hasItem("Process cleanup completed")); + + assertThat(processService.find(context, process_1.getID()), notNullValue()); + assertThat(processService.find(context, process_2.getID()), notNullValue()); + assertThat(processService.find(context, process_3.getID()), notNullValue()); + assertThat(processService.find(context, process_4.getID()), nullValue()); + assertThat(processService.find(context, process_5.getID()), nullValue()); + assertThat(processService.find(context, process_6.getID()), nullValue()); + assertThat(processService.find(context, process_7.getID()), nullValue()); + assertThat(processService.find(context, process_8.getID()), nullValue()); + + } + + private Process buildProcess(ProcessStatus processStatus, Date creationTime) throws SQLException { + return ProcessBuilder.createProcess(context, admin, "test", List.of()) + .withProcessStatus(processStatus) + .withCreationTime(creationTime) + .build(); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java new file mode 100644 index 0000000000..37292e91c8 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CollectionAdministratorsRequestItemStrategyTest.java @@ -0,0 +1,62 @@ +/** + * 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 static org.junit.Assert.assertEquals; + +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.dspace.eperson.Group; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * + * @author Mark H. Wood + */ +public class CollectionAdministratorsRequestItemStrategyTest { + private static final String NAME = "John Q. Public"; + private static final String EMAIL = "jqpublic@example.com"; + + /** + * Test of getRequestItemAuthor method, of class CollectionAdministratorsRequestItemStrategy. + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetRequestItemAuthor() + throws Exception { + System.out.println("getRequestItemAuthor"); + + Context context = Mockito.mock(Context.class); + + EPerson eperson1 = Mockito.mock(EPerson.class); + Mockito.when(eperson1.getEmail()).thenReturn(EMAIL); + Mockito.when(eperson1.getFullName()).thenReturn(NAME); + + Group group1 = Mockito.mock(Group.class); + Mockito.when(group1.getMembers()).thenReturn(List.of(eperson1)); + + Collection collection1 = Mockito.mock(Collection.class); + Mockito.when(collection1.getAdministrators()).thenReturn(group1); + + Item item = Mockito.mock(Item.class); + Mockito.when(item.getOwningCollection()).thenReturn(collection1); + Mockito.when(item.getSubmitter()).thenReturn(eperson1); + + CollectionAdministratorsRequestItemStrategy instance = new CollectionAdministratorsRequestItemStrategy(); + List result = instance.getRequestItemAuthor(context, + item); + assertEquals("Should be one author", 1, result.size()); + assertEquals("Name should match " + NAME, NAME, result.get(0).getFullName()); + assertEquals("Email should match " + EMAIL, EMAIL, result.get(0).getEmail()); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.java new file mode 100644 index 0000000000..c5475612cb --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/requestitem/CombiningRequestItemStrategyTest.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.app.requestitem; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; + +import java.util.List; + +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * + * @author Mark H. Wood + */ +public class CombiningRequestItemStrategyTest { + /** + * Test of getRequestItemAuthor method, of class CombiningRequestItemStrategy. + * @throws java.lang.Exception passed through. + */ + @Test + public void testGetRequestItemAuthor() + throws Exception { + System.out.println("getRequestItemAuthor"); + Context context = null; + + Item item = Mockito.mock(Item.class); + RequestItemAuthor author1 = new RequestItemAuthor("Pat Paulsen", "ppaulsen@example.com"); + RequestItemAuthor author2 = new RequestItemAuthor("Alfred E. Neuman", "aeneuman@example.com"); + RequestItemAuthor author3 = new RequestItemAuthor("Alias Undercover", "aundercover@example.com"); + + RequestItemAuthorExtractor strategy1 = Mockito.mock(RequestItemHelpdeskStrategy.class); + Mockito.when(strategy1.getRequestItemAuthor(context, item)).thenReturn(List.of(author1)); + + RequestItemAuthorExtractor strategy2 = Mockito.mock(RequestItemMetadataStrategy.class); + Mockito.when(strategy2.getRequestItemAuthor(context, item)).thenReturn(List.of(author2, author3)); + + List strategies = List.of(strategy1, strategy2); + + CombiningRequestItemStrategy instance = new CombiningRequestItemStrategy(strategies); + List result = instance.getRequestItemAuthor(context, + item); + assertThat(result, containsInAnyOrder(author1, author2, author3)); + } +} diff --git a/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java new file mode 100644 index 0000000000..df333fa500 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java @@ -0,0 +1,84 @@ +/** + * 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.authorize; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; + +import org.dspace.AbstractIntegrationTest; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit tests for {@link RegexPasswordValidator}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + */ +@RunWith(MockitoJUnitRunner.class) +public class RegexPasswordValidatorTest extends AbstractIntegrationTest { + + @Mock + private ConfigurationService configurationService; + + @InjectMocks + private RegexPasswordValidator regexPasswordValidator; + + @Before + public void setup() { + when(configurationService.getProperty("authentication-password.regex-validation.pattern")) + .thenReturn("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^\\da-zA-Z]).{8,15}$"); + } + + @Test + public void testValidPassword() { + assertThat(regexPasswordValidator.isPasswordValid("TestPassword01!"), is(true)); + } + + @Test + public void testInvalidPasswordForMissingSpecialCharacter() { + assertThat(regexPasswordValidator.isPasswordValid("TestPassword01"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("TestPassword01?"), is(true)); + } + + @Test + public void testInvalidPasswordForMissingNumber() { + assertThat(regexPasswordValidator.isPasswordValid("TestPassword!"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("TestPassword1!"), is(true)); + } + + @Test + public void testInvalidPasswordForMissingUppercaseCharacter() { + assertThat(regexPasswordValidator.isPasswordValid("testpassword01!"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("testPassword01!"), is(true)); + } + + @Test + public void testInvalidPasswordForMissingLowercaseCharacter() { + assertThat(regexPasswordValidator.isPasswordValid("TESTPASSWORD01!"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("TESTPASSWORd01!"), is(true)); + } + + @Test + public void testInvalidPasswordForTooShortValue() { + assertThat(regexPasswordValidator.isPasswordValid("Test01!"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("Test012!"), is(true)); + } + + @Test + public void testInvalidPasswordForTooLongValue() { + assertThat(regexPasswordValidator.isPasswordValid("ThisIsAVeryLongPassword01!"), is(false)); + assertThat(regexPasswordValidator.isPasswordValid("IsAPassword012!"), is(true)); + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java index 62e1d2e7e5..86573940e4 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.sql.SQLException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import java.util.Set; @@ -60,6 +61,11 @@ public class ProcessBuilder extends AbstractBuilder { return this; } + public ProcessBuilder withCreationTime(Date creationTime) { + process.setCreationTime(creationTime); + return this; + } + public ProcessBuilder withStartAndEndTime(String startTime, String endTime) throws ParseException { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy"); process.setStartTime(simpleDateFormat.parse(startTime)); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 6ded477813..942a3062fc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -163,6 +163,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH EPersonNameNotProvidedException.class, GroupNameNotProvidedException.class, GroupHasPendingWorkflowTasksException.class, + PasswordNotValidException.class, }) protected void handleCustomUnprocessableEntityException(HttpServletRequest request, HttpServletResponse response, TranslatableException ex) throws IOException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/PasswordNotValidException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/PasswordNotValidException.java new file mode 100644 index 0000000000..90f30491cc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/PasswordNotValidException.java @@ -0,0 +1,37 @@ +/** + * 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.rest.exception; + +import org.dspace.core.I18nUtil; + +/** + * This class provides an exception to be used when trying to create an EPerson + * with password that not match regular expression configured in this + * variable "authentication-password.regex-validation.pattern" in dspace.cfg or during the patch of password. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class PasswordNotValidException extends UnprocessableEntityException implements TranslatableException { + + private static final long serialVersionUID = -4294543847989250566L; + + public static final String MESSAGE_KEY = "org.dspace.app.rest.exception.PasswordNotValidException.message"; + + public PasswordNotValidException() { + super(I18nUtil.getMessage(MESSAGE_KEY)); + } + + public PasswordNotValidException(Throwable cause) { + super(I18nUtil.getMessage(MESSAGE_KEY), cause); + } + + public String getMessageKey() { + return MESSAGE_KEY; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index cfae12584f..f51abb84c4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -21,9 +21,9 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.rest.DiscoverableEndpointsService; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; -import org.dspace.app.rest.authorization.AuthorizationFeatureService; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.EPersonNameNotProvidedException; +import org.dspace.app.rest.exception.PasswordNotValidException; import org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.EPersonRest; @@ -34,7 +34,7 @@ import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; -import org.dspace.content.service.SiteService; +import org.dspace.authorize.service.ValidatePasswordService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.EmptyWorkflowGroupException; @@ -74,10 +74,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository authorizers; try { - authorizer = requestItemAuthorExtractor.getRequestItemAuthor(context, ri.getItem()); + authorizers = requestItemAuthorExtractor.getRequestItemAuthor(context, ri.getItem()); } catch (SQLException ex) { LOG.warn("Failed to find an authorizer: {}", ex.getMessage()); - authorizer = new RequestItemAuthor("", ""); + authorizers = Collections.EMPTY_LIST; } - if (!authorizer.getEmail().equals(context.getCurrentUser().getEmail())) { + + boolean authorized = false; + String requester = context.getCurrentUser().getEmail(); + for (RequestItemAuthor authorizer : authorizers) { + authorized |= authorizer.getEmail().equals(requester); + } + if (!authorized) { throw new AuthorizeException("Not authorized to approve this request"); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordAddOperation.java index a4f2614dfa..2064bbd843 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordAddOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/EPersonPasswordAddOperation.java @@ -12,9 +12,11 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.PasswordNotValidException; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.ValidatePasswordService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.factory.EPersonServiceFactory; @@ -53,6 +55,9 @@ public class EPersonPasswordAddOperation extends PatchOperation { @Autowired private AccountService accountService; + @Autowired + private ValidatePasswordService validatePasswordService; + @Override public R perform(Context context, R object, Operation operation) { checkOperationValue(operation.getValue()); @@ -66,7 +71,13 @@ public class EPersonPasswordAddOperation extends PatchOperation { if (StringUtils.isNotBlank(token)) { verifyAndDeleteToken(context, eperson, token, operation); } - ePersonService.setPassword(eperson, (String) operation.getValue()); + + String newPassword = (String) operation.getValue(); + if (!validatePasswordService.isPasswordValid(newPassword)) { + throw new PasswordNotValidException(); + } + + ePersonService.setPassword(eperson, newPassword); return object; } else { throw new DSpaceBadRequestException(this.getClass().getName() + " does not support this operation"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerIT.java index 9f7e4e6610..9974d7e725 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/opensearch/OpenSearchControllerIT.java @@ -54,20 +54,7 @@ public class OpenSearchControllerIT extends AbstractControllerIntegrationTest { ; } - // HTML is an open issue in OpenSearch, so skip this test at the moment - @Test - @Ignore - public void searchHtmlTest() throws Exception { - //When we call the root endpoint - getClient().perform(get("/opensearch/search") - .param("query", "cats") - .param("format", "html")) - //The status has to be 200 OK - .andExpect(status().isOk()) - //We expect the content type to be "application/atom+xml;charset=UTF-8" - .andExpect(content().contentType("text/html;charset=UTF-8")) - ; - } + // there is no searchHtmlTest here as the html search is redirected to the angular UI @Test public void searchRssTest() throws Exception { @@ -201,17 +188,23 @@ public class OpenSearchControllerIT extends AbstractControllerIntegrationTest { public void serviceDocumentTest() throws Exception { //When we call the root endpoint getClient().perform(get("/opensearch/service")) - //The status has to be 200 OK - .andExpect(status().isOk()) - // and the contentType has to be an opensearchdescription - .andExpect(content().contentType("application/opensearchdescription+xml;charset=UTF-8")) - // and there need to be some values taken from the test configuration - .andExpect(xpath("OpenSearchDescription/ShortName").string("DSpace")) - .andExpect(xpath("OpenSearchDescription/LongName").string("DSpace at My University")) - .andExpect(xpath("OpenSearchDescription/Description") - .string("DSpace at My University DSpace repository") - ) - ; + // The status has to be 200 OK + .andExpect(status().isOk()) + // and the contentType has to be an opensearchdescription + .andExpect(content().contentType("application/opensearchdescription+xml;charset=UTF-8")) + // and there need to be some values taken from the test configuration + .andExpect(xpath("OpenSearchDescription/ShortName").string("DSpace")) + .andExpect(xpath("OpenSearchDescription/LongName").string("DSpace at My University")) + .andExpect(xpath("OpenSearchDescription/Description") + .string("DSpace at My University DSpace repository")) + .andExpect(xpath("OpenSearchDescription/Url[@type='text/html']/@template") + .string("http://localhost:4000/search?query={searchTerms}")) + .andExpect(xpath("OpenSearchDescription/Url[@type='application/atom+xml; charset=UTF-8']/@template") + .string("http://localhost/opensearch/search?" + + "query={searchTerms}&start={startIndex?}&rpp={count?}&format=atom")) + .andExpect(xpath("OpenSearchDescription/Url[@type='application/rss+xml; charset=UTF-8']/@template") + .string("http://localhost/opensearch/search?" + + "query={searchTerms}&start={startIndex?}&rpp={count?}&format=rss")); /* Expected response for the service document is: @@ -224,9 +217,9 @@ public class OpenSearchControllerIT extends AbstractControllerIntegrationTest { IR DSpace dspace-help@myu.edu http://www.dspace.org/images/favicon.ico - - - + + + */ } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index c341989763..a35310f8d9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -72,17 +72,14 @@ import org.dspace.core.I18nUtil; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.PasswordHash; -import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.RegistrationDataService; import org.dspace.services.ConfigurationService; -import org.dspace.workflow.WorkflowService; import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; - public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired @@ -94,11 +91,6 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired private EPersonService ePersonService; - @Autowired - private WorkflowService workflowService; - - @Autowired - private RegistrationDataDAO registrationDataDAO; @Autowired private ConfigurationService configurationService; @@ -1289,7 +1281,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void patchPassword() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); context.turnOffAuthorisationSystem(); EPerson ePerson = EPersonBuilder.createEPerson(context) @@ -1386,7 +1378,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void patchPasswordForNonAdminUser() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); context.turnOffAuthorisationSystem(); EPerson ePerson = EPersonBuilder.createEPerson(context) @@ -1564,7 +1556,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void patchPasswordNotInitialised() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); context.turnOffAuthorisationSystem(); EPerson ePerson = EPersonBuilder.createEPerson(context) @@ -2009,6 +2001,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void patchReplacePasswordWithToken() throws Exception { + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); context.turnOffAuthorisationSystem(); EPerson ePerson = EPersonBuilder.createEPerson(context) @@ -2222,7 +2215,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void postEPersonWithTokenWithoutEmailProperty() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); ObjectMapper mapper = new ObjectMapper(); String newRegisterEmail = "new-register@fake-email.com"; @@ -2286,7 +2279,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void postEPersonWithTokenWithEmailProperty() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); ObjectMapper mapper = new ObjectMapper(); String newRegisterEmail = "new-register@fake-email.com"; @@ -2348,7 +2341,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void postEPersonWithTokenWithEmailAndSelfRegisteredProperty() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); ObjectMapper mapper = new ObjectMapper(); String newRegisterEmail = "new-register@fake-email.com"; @@ -2801,7 +2794,7 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void postEPersonWithTokenWithEmailPropertyAnonUser() throws Exception { - + configurationService.setProperty("authentication-password.regex-validation.pattern", ""); ObjectMapper mapper = new ObjectMapper(); String newRegisterEmail = "new-register@fake-email.com"; @@ -3129,4 +3122,199 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { } -} + @Test + public void validatePasswordRobustnessContainingAtLeastAnUpperCaseCharUnprocessableTest() throws Exception { + configurationService.setProperty("authentication-password.regex-validation.pattern", "^(?=.*[A-Z])"); + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Misha"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("Boychuk"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("lowercasepassword"); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); + + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnprocessableEntity()); + + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + } + } + + @Test + public void validatePasswordRobustnessContainingAtLeastAnUpperCaseCharTest() throws Exception { + configurationService.setProperty("authentication-password.regex-validation.pattern", "^(?=.*[A-Z])"); + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + + getClient().perform(post("/api/eperson/registrations") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + + String newRegisterToken = registrationDataService.findByEmail(context, newRegisterEmail).getToken(); + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setCanLogIn(true); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Boychuk"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("Misha"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + ePersonRest.setPassword("Lowercasepassword"); + AtomicReference idRef = new AtomicReference(); + + mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess()); + + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "Misha"), + matchMetadata("eperson.lastname", "Boychuk")))))) + .andDo(result -> idRef.set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); + + EPerson createdEPerson = ePersonService.find(context, UUID.fromString(String.valueOf(idRef.get()))); + assertTrue(ePersonService.checkPassword(context, createdEPerson, "Lowercasepassword")); + assertNull(registrationDataService.findByToken(context, newRegisterToken)); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.deleteByToken(context, newRegisterToken); + context.restoreAuthSystemState(); + EPersonBuilder.deleteEPerson(idRef.get()); + } + } + + @Test + public void validatePasswordRobustnessContainingAtLeastAnUppercaseCharPatchUnprocessableTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@example.com") + .withPassword("TestPassword") + .build(); + + context.restoreAuthSystemState(); + + configurationService.setProperty("authentication-password.regex-validation.pattern", "^(?=.*[A-Z])"); + + String newPassword = "newpassword"; + + List ops = new ArrayList(); + AddOperation addOperation = new AddOperation("/password", newPassword); + ops.add(addOperation); + String patchBody = getPatchContent(ops); + + String token = getAuthToken(admin.getEmail(), password); + + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + // can't login with new password + token = getAuthToken(ePerson.getEmail(), newPassword); + getClient(token).perform(get("/api/authn/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(false))) + .andExpect(jsonPath("$.type", is("status"))); + + // login with origin password + token = getAuthToken(ePerson.getEmail(), "TestPassword"); + getClient(token).perform(get("/api/authn/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.type", is("status"))); + } + + @Test + public void validatePasswordRobustnessContainingAtLeastAnUppercaseCharPatchTest() throws Exception { + configurationService.setProperty("authentication-password.regex-validation.pattern", "^(?=.*[A-Z])"); + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("John", "Doe") + .withEmail("Johndoe@example.com") + .withPassword("TestPassword") + .build(); + + context.restoreAuthSystemState(); + + String newPassword = "Newpassword"; + + List ops = new ArrayList(); + AddOperation addOperation = new AddOperation("/password", newPassword); + ops.add(addOperation); + String patchBody = getPatchContent(ops); + + String token = getAuthToken(admin.getEmail(), password); + + // updates password + getClient(token).perform(patch("/api/eperson/epersons/" + ePerson.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + // login with new password + token = getAuthToken(ePerson.getEmail(), newPassword); + getClient(token).perform(get("/api/authn/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.type", is("status"))); + + // can't login with old password + token = getAuthToken(ePerson.getEmail(), "TestPassword"); + getClient(token).perform(get("/api/authn/status")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(false))) + .andExpect(jsonPath("$.type", is("status"))); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 9ad5af09c2..4a9f03ffff 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -994,6 +994,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration } bibtex.close(); } + @Test /** * Test the creation of workspaceitems POSTing to the resource collection endpoint a bibtex file @@ -1187,6 +1188,112 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration bibtex.close(); } + @Test + /** + * Test the creation of workspaceitems POSTing to the resource collection endpoint a bibtex file + * + * @throws Exception + */ + public void createSingleWorkspaceItemFromBibtexFileWithMultipleAuthorsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .withSubmitterGroup(eperson) + .build(); + Collection col2 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 2") + .withSubmitterGroup(eperson) + .build(); + + InputStream bibtex = getClass().getResourceAsStream("bibtex-test-multiple-authors.bib"); + final MockMultipartFile bibtexFile = new MockMultipartFile("file", + "/local/path/bibtex-test-multiple-authors.bib", + "application/x-bibtex", bibtex); + + context.restoreAuthSystemState(); + + AtomicReference> idRef = new AtomicReference<>(); + String authToken = getAuthToken(eperson.getEmail(), password); + try { + // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) + getClient(authToken).perform(multipart("/api/submission/workspaceitems") + .file(bibtexFile)) + // create should return 200, 201 (created) is better for single resource + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.workspaceitems[0]" + + ".sections.traditionalpageone['dc.title'][0].value", + is("My Article"))) + .andExpect(jsonPath("$._embedded.workspaceitems[0]" + + ".sections.traditionalpageone['dc.contributor.author'][0].value", + is("A. Nauthor"))) + .andExpect(jsonPath("$._embedded.workspaceitems[0]" + + ".sections.traditionalpageone['dc.contributor.author'][1].value", + is("A. Nother"))) + .andExpect(jsonPath("$._embedded.workspaceitems[0]" + + ".sections.traditionalpageone['dc.contributor.author'][2].value", + is("A. Third"))) + .andExpect( + jsonPath("$._embedded.workspaceitems[0]._embedded.collection.id", + is(col1.getID().toString()))) + .andExpect(jsonPath("$._embedded.workspaceitems[0].sections.upload.files[0]" + + ".metadata['dc.source'][0].value", + is("/local/path/bibtex-test-multiple-authors.bib"))) + .andExpect(jsonPath("$._embedded.workspaceitems[0].sections.upload.files[0]" + + ".metadata['dc.title'][0].value", + is("bibtex-test-multiple-authors.bib"))) + .andExpect( + jsonPath("$._embedded.workspaceitems[*]._embedded.upload").doesNotExist()) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), + "$._embedded.workspaceitems[*].id"))); + } finally { + if (idRef != null && idRef.get() != null) { + for (int i : idRef.get()) { + WorkspaceItemBuilder.deleteWorkspaceItem(i); + } + } + } + + // create a workspaceitem from a single bibliographic entry file explicitly in the col2 + try { + getClient(authToken).perform(multipart("/api/submission/workspaceitems") + .file(bibtexFile) + .param("owningCollection", col2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.workspaceitems[0]" + + ".sections.traditionalpageone['dc.title'][0].value", + is("My Article"))) + .andExpect( + jsonPath("$._embedded.workspaceitems[0]._embedded.collection.id", + is(col2.getID().toString()))) + .andExpect(jsonPath("$._embedded.workspaceitems[0].sections.upload.files[0]" + + ".metadata['dc.source'][0].value", + is("/local/path/bibtex-test-multiple-authors.bib"))) + .andExpect(jsonPath("$._embedded.workspaceitems[0].sections.upload" + + ".files[0].metadata['dc.title'][0].value", + is("bibtex-test-multiple-authors.bib"))) + .andExpect( + jsonPath("$._embedded.workspaceitems[*]._embedded.upload").doesNotExist()) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), + "$._embedded.workspaceitems[*].id"))); + } finally { + if (idRef != null && idRef.get() != null) { + for (int i : idRef.get()) { + WorkspaceItemBuilder.deleteWorkspaceItem(i); + } + } + } + bibtex.close(); + } + @Test /** * Test the creation of workspaceitems POSTing to the resource collection endpoint a csv file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java index fb8b44fa17..e020c04b1a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/eperson/DeleteEPersonSubmitterIT.java @@ -142,10 +142,10 @@ public class DeleteEPersonSubmitterIT extends AbstractControllerIntegrationTest Item item = itemService.find(context, installItem.getID()); - RequestItemAuthor requestItemAuthor = requestItemAuthorExtractor.getRequestItemAuthor(context, item); + List requestItemAuthor = requestItemAuthorExtractor.getRequestItemAuthor(context, item); - assertEquals("Help Desk", requestItemAuthor.getFullName()); - assertEquals("dspace-help@myu.edu", requestItemAuthor.getEmail()); + assertEquals("Help Desk", requestItemAuthor.get(0).getFullName()); + assertEquals("dspace-help@myu.edu", requestItemAuthor.get(0).getEmail()); } /** @@ -171,7 +171,7 @@ public class DeleteEPersonSubmitterIT extends AbstractControllerIntegrationTest Item item = installItemService.installItem(context, wsi); - List opsToWithDraw = new ArrayList(); + List opsToWithDraw = new ArrayList<>(); ReplaceOperation replaceOperationToWithDraw = new ReplaceOperation("/withdrawn", true); opsToWithDraw.add(replaceOperationToWithDraw); String patchBodyToWithdraw = getPatchContent(opsToWithDraw); @@ -191,7 +191,7 @@ public class DeleteEPersonSubmitterIT extends AbstractControllerIntegrationTest assertNull(retrieveItemSubmitter(item.getID())); - List opsToReinstate = new ArrayList(); + List opsToReinstate = new ArrayList<>(); ReplaceOperation replaceOperationToReinstate = new ReplaceOperation("/withdrawn", false); opsToReinstate.add(replaceOperationToReinstate); String patchBodyToReinstate = getPatchContent(opsToReinstate); diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/bibtex-test-multiple-authors.bib b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/bibtex-test-multiple-authors.bib new file mode 100644 index 0000000000..1db78de377 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/bibtex-test-multiple-authors.bib @@ -0,0 +1,4 @@ +@misc{ Nobody01, + author = "A. Nauthor and A. Nother and A. Third", + title = "My Article", + year = "2006" } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 8ee956fe50..4a4cbc4bc4 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1320,10 +1320,10 @@ webui.feed.item.author = dc.contributor.author # so even if Syndication Feeds are not enabled, they must be configured # enable open search websvc.opensearch.enable = true -# url used in service document -websvc.opensearch.svccontext = opensearch # context for html request URLs - change only for non-standard servlet mapping -websvc.opensearch.uicontext = simple-search +websvc.opensearch.uicontext = search +# context for xml request URLs - change only for non-standard servlet mapping +websvc.opensearch.svccontext = opensearch/search # present autodiscovery link in every page head websvc.opensearch.autolink = true # number of hours to retain results before recalculating @@ -1342,8 +1342,8 @@ websvc.opensearch.samplequery = photosynthesis # tags used to describe search service websvc.opensearch.tags = IR DSpace # result formats offered - use 1 or more comma-separated from: html,atom,rss -# NB: html is not supported in DSpace7, use normal search module instead -websvc.opensearch.formats = atom,rss +# html uses the normal search module +websvc.opensearch.formats = html,atom,rss #### Content Inline Disposition Threshold #### @@ -1557,6 +1557,13 @@ solr-database-resync.time-until-reindex = 600000 # Keep in mind, changing the schedule requires rebooting your servlet container, e.g. Tomcat. solr-database-resync.cron = 0 15 2 * * ? +#----------------------------------------------------------# +#----------PROCESS CLEANER SCRIPT CONFIGURATION------------# +#----------------------------------------------------------# +# Processes older than this number of days will be deleted when the "process-cleaner" script is next run. +# Default is 14 (i.e. processes that are two weeks or older will be deleted) +# process-cleaner.days = 14 + #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# diff --git a/dspace/config/modules/authentication-password.cfg b/dspace/config/modules/authentication-password.cfg index 07aaeed14d..078da7f8d1 100644 --- a/dspace/config/modules/authentication-password.cfg +++ b/dspace/config/modules/authentication-password.cfg @@ -26,3 +26,24 @@ # SHA-256, SHA-384, and SHA-512 should be available, but you may have # installed others. If not set, SHA-512 will be used. # authentication-password.digestAlgorithm = SHA-512 + +###### Validate Password Robustness Configuration ###### +# (by default is enabled, to disable, either comment out this configuration or set it to an empty value) +# This regular expression is used to validate password during creation of EPerson +# or during the patch of password. +# NOTE: when you configure a custom regex, you will also need to update the text of +# "org.dspace.app.rest.exception.PasswordNotValidException.message" in Messages.properties to describe the minimum requirements. +# +# The following regex applies subsequent rules: ^(?=.*?[a-z])(?=.*?[A-Z])(?=\\S*?[0-9])(?=\\S*?[!?$@#$%^&+=]).{8\,15}$ +# 1) (?=.*?[a-z]) - the password must contain at least one lowercase character +# 2) (?=.*?[A-Z]) - the password must contain at least one uppercase character +# 3) (?=\\S*?[0-9]) - the password must contain at least one numeric character +# 4) (?=\\S*?[!?$@#$%^&+=]) - the password must contain at least one of the following special character: !?$@#$%^&+= +# 5) {8\,15} - the password must be at least 8 and at most 15 characters long +# REMARK: {8\,15} - the slash in this regex is an exception of the Apache library, as "," is a special character, +# consequently to interpret it correctly you have to add the slash in front + +# By default, DSpace just requires a password of 8 or more characters. +# However, we recommend most sites consider either increasing the required length or complexity (see example above) +authentication-password.regex-validation.pattern = ^.{8\,}$ + diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index b146bc6c2f..1d7b87960d 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -15,6 +15,7 @@ plugin.named.org.dspace.curate.CurationTask = org.dspace.ctask.general.RequiredM #plugin.named.org.dspace.curate.CurationTask = org.dspace.ctask.general.MicrosoftTranslator = translate plugin.named.org.dspace.curate.CurationTask = org.dspace.ctask.general.MetadataValueLinkChecker = checklinks plugin.named.org.dspace.curate.CurationTask = org.dspace.ctask.general.RegisterDOI = registerdoi +#plugin.named.org.dspace.curate.CurationTask = org.dspace.ctask.general.CitationPage = citationpage # add new tasks here (or in additional config files) ## task queue implementation diff --git a/dspace/config/spring/api/bibtex-integration.xml b/dspace/config/spring/api/bibtex-integration.xml index 2ad267dd30..bc5adf5886 100644 --- a/dspace/config/spring/api/bibtex-integration.xml +++ b/dspace/config/spring/api/bibtex-integration.xml @@ -40,9 +40,14 @@ - - - + + + + + + + + diff --git a/dspace/config/spring/api/core-factory-services.xml b/dspace/config/spring/api/core-factory-services.xml index 44a9b103bd..20e5297b6c 100644 --- a/dspace/config/spring/api/core-factory-services.xml +++ b/dspace/config/spring/api/core-factory-services.xml @@ -44,6 +44,8 @@ + + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index de55e16031..eaee562681 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -143,5 +143,8 @@ + + + diff --git a/dspace/config/spring/api/external-openaire.xml b/dspace/config/spring/api/external-openaire.xml index f483ce7210..25a23a1739 100644 --- a/dspace/config/spring/api/external-openaire.xml +++ b/dspace/config/spring/api/external-openaire.xml @@ -1,6 +1,10 @@ - @@ -10,17 +14,65 @@ - + + + Project - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dspace/config/spring/api/requestitem.xml b/dspace/config/spring/api/requestitem.xml index cc18c7916f..90c49156d5 100644 --- a/dspace/config/spring/api/requestitem.xml +++ b/dspace/config/spring/api/requestitem.xml @@ -8,25 +8,65 @@ http://www.springframework.org/schema/context/spring-context-2.5.xsd" default-autowire-candidates="*Service,*DAO,javax.sql.DataSource"> + + Strategies for determining who receives Request Copy emails. + A copy request "strategy" class produces a list of addresses to which a + request email should be sent. Each strategy gets its addresses from a + different source. Select the one that meets your need, or use the + CombiningRequestItemStrategy to meld the lists from two or more other + strategies. + + - - + - - - --> + + + + - + + + + + + diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index d5f869316d..ec7c0cafcc 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -50,6 +50,11 @@ + + + + + diff --git a/dspace/config/spring/rest/scripts.xml b/dspace/config/spring/rest/scripts.xml index 977f5d8cd4..c5b4717fbc 100644 --- a/dspace/config/spring/rest/scripts.xml +++ b/dspace/config/spring/rest/scripts.xml @@ -39,6 +39,11 @@ + + + + +