diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 546cbf01f3..547be787e4 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -769,7 +769,7 @@ org.json json - 20230227 + 20231013 diff --git a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java index 1cacbf6aed..2d782dc3b8 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java +++ b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java @@ -22,9 +22,21 @@ public interface AccessStatusHelper { * * @param context the DSpace context * @param item the item + * @param threshold the embargo threshold date * @return an access status value * @throws SQLException An exception that provides information on a database access error or other errors. */ public String getAccessStatusFromItem(Context context, Item item, Date threshold) throws SQLException; + + /** + * Retrieve embargo information for the item + * + * @param context the DSpace context + * @param item the item to check for embargo information + * @param threshold the embargo threshold date + * @return an embargo date + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getEmbargoFromItem(Context context, Item item, Date threshold) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java index 1ce3673452..01b3707479 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java @@ -67,4 +67,9 @@ public class AccessStatusServiceImpl implements AccessStatusService { public String getAccessStatus(Context context, Item item) throws SQLException { return helper.getAccessStatusFromItem(context, item, forever_date); } + + @Override + public String getEmbargoFromItem(Context context, Item item) throws SQLException { + return helper.getEmbargoFromItem(context, item, forever_date); + } } diff --git a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java index a67fa67af3..5f0e6d8b25 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java +++ b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java @@ -26,6 +26,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.Group; +import org.joda.time.LocalDate; /** * Default plugin implementation of the access status helper. @@ -33,6 +34,11 @@ import org.dspace.eperson.Group; * calculate the access status of an item based on the policies of * the primary or the first bitstream in the original bundle. * Users can override this method for enhanced functionality. + * + * The getEmbargoInformationFromItem method provides a simple logic to + * * retrieve embargo information of bitstreams from an item based on the policies of + * * the primary or the first bitstream in the original bundle. + * * Users can override this method for enhanced functionality. */ public class DefaultAccessStatusHelper implements AccessStatusHelper { public static final String EMBARGO = "embargo"; @@ -54,12 +60,12 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper { /** * Look at the item's policies to determine an access status value. - * It is also considering a date threshold for embargos and restrictions. + * It is also considering a date threshold for embargoes and restrictions. * * If the item is null, simply returns the "unknown" value. * * @param context the DSpace context - * @param item the item to embargo + * @param item the item to check for embargoes * @param threshold the embargo threshold date * @return an access status value */ @@ -86,7 +92,7 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper { .findFirst() .orElse(null); } - return caculateAccessStatusForDso(context, bitstream, threshold); + return calculateAccessStatusForDso(context, bitstream, threshold); } /** @@ -104,7 +110,7 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper { * @param threshold the embargo threshold date * @return an access status value */ - private String caculateAccessStatusForDso(Context context, DSpaceObject dso, Date threshold) + private String calculateAccessStatusForDso(Context context, DSpaceObject dso, Date threshold) throws SQLException { if (dso == null) { return METADATA_ONLY; @@ -156,4 +162,87 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper { } return RESTRICTED; } + + /** + * Look at the policies of the primary (or first) bitstream of the item to retrieve its embargo. + * + * If the item is null, simply returns an empty map with no embargo information. + * + * @param context the DSpace context + * @param item the item to embargo + * @return an access status value + */ + @Override + public String getEmbargoFromItem(Context context, Item item, Date threshold) + throws SQLException { + Date embargoDate; + + // If Item status is not "embargo" then return a null embargo date. + String accessStatus = getAccessStatusFromItem(context, item, threshold); + + if (item == null || !accessStatus.equals(EMBARGO)) { + return null; + } + // Consider only the original bundles. + List bundles = item.getBundles(Constants.DEFAULT_BUNDLE_NAME); + // Check for primary bitstreams first. + Bitstream bitstream = bundles.stream() + .map(bundle -> bundle.getPrimaryBitstream()) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (bitstream == null) { + // If there is no primary bitstream, + // take the first bitstream in the bundles. + bitstream = bundles.stream() + .map(bundle -> bundle.getBitstreams()) + .flatMap(List::stream) + .findFirst() + .orElse(null); + } + + if (bitstream == null) { + return null; + } + + embargoDate = this.retrieveShortestEmbargo(context, bitstream); + + return embargoDate != null ? embargoDate.toString() : null; + } + + /** + * + */ + private Date retrieveShortestEmbargo(Context context, Bitstream bitstream) throws SQLException { + Date embargoDate = null; + // Only consider read policies. + List policies = authorizeService + .getPoliciesActionFilter(context, bitstream, Constants.READ); + + // Looks at all read policies. + for (ResourcePolicy policy : policies) { + boolean isValid = resourcePolicyService.isDateValid(policy); + Group group = policy.getGroup(); + + if (group != null && StringUtils.equals(group.getName(), Group.ANONYMOUS)) { + // Only calculate the status for the anonymous group. + if (!isValid) { + // If the policy is not valid there is an active embargo + Date startDate = policy.getStartDate(); + + if (startDate != null && !startDate.before(LocalDate.now().toDate())) { + // There is an active embargo: aim to take the shortest embargo (account for rare cases where + // more than one resource policy exists) + if (embargoDate == null) { + embargoDate = startDate; + } else { + embargoDate = startDate.before(embargoDate) ? startDate : embargoDate; + } + } + } + } + } + + return embargoDate; + } } diff --git a/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java index 43de5e3c47..2ed47bde4c 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java +++ b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java @@ -40,7 +40,18 @@ public interface AccessStatusService { * * @param context the DSpace context * @param item the item + * @return an access status value * @throws SQLException An exception that provides information on a database access error or other errors. */ public String getAccessStatus(Context context, Item item) throws SQLException; + + /** + * Retrieve embargo information for the item + * + * @param context the DSpace context + * @param item the item to check for embargo information + * @return an embargo date + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getEmbargoFromItem(Context context, Item item) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java index 50e1022dbe..7bef232f04 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java @@ -464,7 +464,7 @@ public class BulkAccessControl extends DSpaceRunnable createResourcePolicy(item, accessCondition, itemAccessConditions.get(accessCondition.getName()))); - itemService.adjustItemPolicies(context, item, item.getOwningCollection()); + itemService.adjustItemPolicies(context, item, item.getOwningCollection(), false); } /** diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index 4161bbb4d8..af6976acb1 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -578,6 +578,10 @@ public class MetadataImport extends DSpaceRunnable imp @Override public Bitstream getThumbnail(Context context, Bitstream bitstream) throws SQLException { - Pattern pattern = Pattern.compile("^" + bitstream.getName() + ".([^.]+)$"); + Pattern pattern = getBitstreamNamePattern(bitstream); for (Bundle bundle : bitstream.getBundles()) { for (Item item : bundle.getItems()) { @@ -420,6 +420,13 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl imp return null; } + protected Pattern getBitstreamNamePattern(Bitstream bitstream) { + if (bitstream.getName() != null) { + return Pattern.compile("^" + Pattern.quote(bitstream.getName()) + ".([^.]+)$"); + } + return Pattern.compile("^" + bitstream.getName() + ".([^.]+)$"); + } + @Override public BitstreamFormat getFormat(Context context, Bitstream bitstream) throws SQLException { if (bitstream.getBitstreamFormat() == null) { diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java index 32c5b92c60..1aadbea162 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java @@ -93,7 +93,7 @@ public class InstallItemServiceImpl implements InstallItemService { // As this is a BRAND NEW item, as a final step we need to remove the // submitter item policies created during deposit and replace them with // the default policies from the collection. - itemService.inheritCollectionDefaultPolicies(c, item, collection); + itemService.inheritCollectionDefaultPolicies(c, item, collection, false); return item; } @@ -271,4 +271,28 @@ public class InstallItemServiceImpl implements InstallItemService { return myMessage.toString(); } + + @Override + public String getSubmittedByProvenanceMessage(Context context, Item item) throws SQLException { + // get date + DCDate now = DCDate.getCurrent(); + + // Create provenance description + StringBuffer provmessage = new StringBuffer(); + + if (item.getSubmitter() != null) { + provmessage.append("Submitted by ").append(item.getSubmitter().getFullName()) + .append(" (").append(item.getSubmitter().getEmail()).append(") on ") + .append(now.toString()); + } else { + // else, null submitter + provmessage.append("Submitted by unknown (probably automated) on") + .append(now.toString()); + } + provmessage.append("\n"); + + // add sizes and checksums of bitstreams + provmessage.append(getBitstreamProvenanceMessage(context, item)); + return provmessage.toString(); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 8d1ba14b2c..ebea2aa5b8 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -920,8 +920,16 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Override public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { - adjustItemPolicies(context, item, collection); - adjustBundleBitstreamPolicies(context, item, collection); + inheritCollectionDefaultPolicies(context, item, collection, true); + } + + @Override + public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException { + + adjustItemPolicies(context, item, collection, replaceReadRPWithCollectionRP); + adjustBundleBitstreamPolicies(context, item, collection, replaceReadRPWithCollectionRP); log.debug(LogHelper.getHeader(context, "item_inheritCollectionDefaultPolicies", "item_id=" + item.getID())); @@ -930,6 +938,13 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Override public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { + adjustBundleBitstreamPolicies(context, item, collection, true); + } + + @Override + public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException { // Bundles should inherit from DEFAULT_ITEM_READ so that if the item is readable, the files // can be listed (even if they are themselves not readable as per DEFAULT_BITSTREAM_READ or other // policies or embargos applied @@ -948,10 +963,19 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It } // TODO: should we also throw an exception if no DEFAULT_ITEM_READ? + boolean removeCurrentReadRPBitstream = + replaceReadRPWithCollectionRP && defaultCollectionBitstreamPolicies.size() > 0; + boolean removeCurrentReadRPBundle = + replaceReadRPWithCollectionRP && defaultCollectionBundlePolicies.size() > 0; + // remove all policies from bundles, add new ones // Remove bundles List bunds = item.getBundles(); for (Bundle mybundle : bunds) { + // If collection has default READ policies, remove the bundle's READ policies. + if (removeCurrentReadRPBundle) { + authorizeService.removePoliciesActionFilter(context, mybundle, Constants.READ); + } // if come from InstallItem: remove all submission/workflow policies authorizeService.removeAllPoliciesByDSOAndType(context, mybundle, ResourcePolicy.TYPE_SUBMISSION); @@ -960,6 +984,11 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It addDefaultPoliciesNotInPlace(context, mybundle, defaultCollectionBundlePolicies); for (Bitstream bitstream : mybundle.getBitstreams()) { + // If collection has default READ policies, remove the bundle's READ policies. + if (removeCurrentReadRPBitstream) { + authorizeService.removePoliciesActionFilter(context, bitstream, Constants.READ); + } + // if come from InstallItem: remove all submission/workflow policies removeAllPoliciesAndAddDefault(context, bitstream, defaultItemPolicies, defaultCollectionBitstreamPolicies); @@ -968,7 +997,14 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It } @Override - public void adjustBitstreamPolicies(Context context, Item item, Collection collection , Bitstream bitstream) + public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream) + throws SQLException, AuthorizeException { + adjustBitstreamPolicies(context, item, collection, bitstream, true); + } + + @Override + public void adjustBitstreamPolicies(Context context, Item item, Collection collection , Bitstream bitstream, + boolean replaceReadRPWithCollectionRP) throws SQLException, AuthorizeException { List defaultCollectionPolicies = authorizeService .getPoliciesActionFilter(context, collection, Constants.DEFAULT_BITSTREAM_READ); @@ -998,10 +1034,22 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Override public void adjustItemPolicies(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { + adjustItemPolicies(context, item, collection, true); + } + + @Override + public void adjustItemPolicies(Context context, Item item, Collection collection, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException { // read collection's default READ policies List defaultCollectionPolicies = authorizeService .getPoliciesActionFilter(context, collection, Constants.DEFAULT_ITEM_READ); + // If collection has defaultREAD policies, remove the item's READ policies. + if (replaceReadRPWithCollectionRP && defaultCollectionPolicies.size() > 0) { + authorizeService.removePoliciesActionFilter(context, item, Constants.READ); + } + // MUST have default policies if (defaultCollectionPolicies.size() < 1) { throw new SQLException("Collection " + collection.getID() diff --git a/dspace-api/src/main/java/org/dspace/content/service/InstallItemService.java b/dspace-api/src/main/java/org/dspace/content/service/InstallItemService.java index 67ac2e2049..d00c62cc91 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/InstallItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/InstallItemService.java @@ -83,4 +83,15 @@ public interface InstallItemService { public String getBitstreamProvenanceMessage(Context context, Item myitem) throws SQLException; + /** + * Generate provenance description of direct item submission (not through workflow). + * + * @param context context + * @param item the item to generate description for + * @return provenance description + * @throws SQLException if database error + */ + public String getSubmittedByProvenanceMessage(Context context, Item item) + throws SQLException;; + } diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index b6bf7aa5cf..de7644af83 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -473,7 +473,7 @@ public interface ItemService public void removeGroupPolicies(Context context, Item item, Group group) throws SQLException, AuthorizeException; /** - * remove all policies on an item and its contents, and replace them with + * Remove all policies on an item and its contents, and replace them with * the DEFAULT_ITEM_READ and DEFAULT_BITSTREAM_READ policies belonging to * the collection. * @@ -488,6 +488,26 @@ public interface ItemService public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection) throws java.sql.SQLException, AuthorizeException; + /** + * Remove all submission and workflow policies on an item and its contents, and add + * default collection policies which are not yet already in place. + * If overrideItemReadPolicies is true, then all read policies on the item are replaced (but only if the + * collection has a default read policy). + * + * @param context DSpace context object + * @param item item to reset policies on + * @param collection Collection + * @param overrideItemReadPolicies if true, all read policies on the item are replaced (but only if the + * collection has a default read policy) + * @throws SQLException if database error + * if an SQL error or if no default policies found. It's a bit + * draconian, but default policies must be enforced. + * @throws AuthorizeException if authorization error + */ + public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection, + boolean overrideItemReadPolicies) + throws java.sql.SQLException, AuthorizeException; + /** * Adjust the Bundle and Bitstream policies to reflect what have been defined * during the submission/workflow. The temporary SUBMISSION and WORKFLOW @@ -507,6 +527,28 @@ public interface ItemService public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection) throws SQLException, AuthorizeException; + /** + * Adjust the Bundle and Bitstream policies to reflect what have been defined + * during the submission/workflow. The temporary SUBMISSION and WORKFLOW + * policies are removed and the policies defined at the item and collection + * level are copied and inherited as appropriate. Custom selected Item policies + * are copied to the bundle/bitstream only if no explicit custom policies were + * already applied to the bundle/bitstream. Collection's policies are inherited + * if there are no other policies defined or if the append mode is defined by + * the configuration via the core.authorization.installitem.inheritance-read.append-mode property + * + * @param context DSpace context object + * @param item Item to adjust policies on + * @param collection Collection + * @param replaceReadRPWithCollectionRP if true, all read policies on the item are replaced (but only if the + * collection has a default read policy) + * @throws SQLException If database error + * @throws AuthorizeException If authorization error + */ + public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException; + /** * Adjust the Bitstream policies to reflect what have been defined * during the submission/workflow. The temporary SUBMISSION and WORKFLOW @@ -527,6 +569,29 @@ public interface ItemService public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream) throws SQLException, AuthorizeException; + /** + * Adjust the Bitstream policies to reflect what have been defined + * during the submission/workflow. The temporary SUBMISSION and WORKFLOW + * policies are removed and the policies defined at the item and collection + * level are copied and inherited as appropriate. Custom selected Item policies + * are copied to the bitstream only if no explicit custom policies were + * already applied to the bitstream. Collection's policies are inherited + * if there are no other policies defined or if the append mode is defined by + * the configuration via the core.authorization.installitem.inheritance-read.append-mode property + * + * @param context DSpace context object + * @param item Item to adjust policies on + * @param collection Collection + * @param bitstream Bitstream to adjust policies on + * @param replaceReadRPWithCollectionRP If true, all read policies on the bitstream are replaced (but only if the + * collection has a default read policy) + * @throws SQLException If database error + * @throws AuthorizeException If authorization error + */ + public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException; + /** * Adjust the Item's policies to reflect what have been defined during the @@ -545,6 +610,26 @@ public interface ItemService public void adjustItemPolicies(Context context, Item item, Collection collection) throws SQLException, AuthorizeException; + /** + * Adjust the Item's policies to reflect what have been defined during the + * submission/workflow. The temporary SUBMISSION and WORKFLOW policies are + * removed and the default policies defined at the collection level are + * inherited as appropriate. Collection's policies are inherited if there are no + * other policies defined or if the append mode is defined by the configuration + * via the core.authorization.installitem.inheritance-read.append-mode property + * + * @param context DSpace context object + * @param item Item to adjust policies on + * @param collection Collection + * @param replaceReadRPWithCollectionRP If true, all read policies on the item are replaced (but only if the + * collection has a default read policy) + * @throws SQLException If database error + * @throws AuthorizeException If authorization error + */ + public void adjustItemPolicies(Context context, Item item, Collection collection, + boolean replaceReadRPWithCollectionRP) + throws SQLException, AuthorizeException; + /** * Moves the item from one collection to another one * @@ -790,24 +875,24 @@ public interface ItemService int countWithdrawnItems(Context context) throws SQLException; /** - * finds all items for which the current user has editing rights - * @param context DSpace context object - * @param offset page offset - * @param limit page size limit - * @return list of items for which the current user has editing rights - * @throws SQLException - * @throws SearchServiceException - */ + * finds all items for which the current user has editing rights + * @param context DSpace context object + * @param offset page offset + * @param limit page size limit + * @return list of items for which the current user has editing rights + * @throws SQLException + * @throws SearchServiceException + */ public List findItemsWithEdit(Context context, int offset, int limit) throws SQLException, SearchServiceException; /** - * counts all items for which the current user has editing rights - * @param context DSpace context object - * @return list of items for which the current user has editing rights - * @throws SQLException - * @throws SearchServiceException - */ + * counts all items for which the current user has editing rights + * @param context DSpace context object + * @return list of items for which the current user has editing rights + * @throws SQLException + * @throws SearchServiceException + */ public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; /** diff --git a/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java index 8324105a30..d895f9a764 100644 --- a/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java @@ -17,9 +17,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import javax.servlet.http.HttpServletRequest; import org.dspace.core.service.LicenseService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.services.model.Request; +import org.dspace.web.ContextUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,13 +104,14 @@ public class LicenseServiceImpl implements LicenseService { /** * Get the site-wide default license that submitters need to grant * + * Localized license requires: default_{{locale}}.license file. + * Locale also must be listed in webui.supported.locales setting. + * * @return the default license */ @Override public String getDefaultSubmissionLicense() { - if (null == license) { - init(); - } + init(); return license; } @@ -115,9 +119,8 @@ public class LicenseServiceImpl implements LicenseService { * Load in the default license. */ protected void init() { - File licenseFile = new File( - DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("dspace.dir") - + File.separator + "config" + File.separator + "default.license"); + Context context = obtainContext(); + File licenseFile = new File(I18nUtil.getDefaultLicense(context)); FileInputStream fir = null; InputStreamReader ir = null; @@ -169,4 +172,24 @@ public class LicenseServiceImpl implements LicenseService { } } } + + /** + * Obtaining current request context. + * Return new context if getting one from current request failed. + * + * @return DSpace context object + */ + private Context obtainContext() { + try { + Request currentRequest = DSpaceServicesFactory.getInstance().getRequestService().getCurrentRequest(); + if (currentRequest != null) { + HttpServletRequest request = currentRequest.getHttpServletRequest(); + return ContextUtil.obtainContext(request); + } + } catch (Exception e) { + log.error("Can't load current request context."); + } + + return new Context(); + } } diff --git a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java index f45f4a17b6..00e91ee1fb 100644 --- a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; @@ -31,6 +32,7 @@ import org.dspace.workflow.CurationTaskConfig; import org.dspace.workflow.FlowStep; import org.dspace.workflow.Task; import org.dspace.workflow.TaskSet; +import org.dspace.xmlworkflow.Role; import org.dspace.xmlworkflow.RoleMembers; import org.dspace.xmlworkflow.WorkflowConfigurationException; import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; @@ -48,14 +50,17 @@ import org.springframework.stereotype.Service; * Manage interactions between curation and workflow. A curation task can be * attached to a workflow step, to be executed during the step. * + *

+ * NOTE: when run in workflow, curation tasks run with + * authorization disabled. + * * @see CurationTaskConfig * @author mwood */ @Service public class XmlWorkflowCuratorServiceImpl implements XmlWorkflowCuratorService { - private static final Logger LOG - = org.apache.logging.log4j.LogManager.getLogger(); + private static final Logger LOG = LogManager.getLogger(); @Autowired(required = true) protected XmlWorkflowFactory workflowFactory; @@ -98,7 +103,18 @@ public class XmlWorkflowCuratorServiceImpl throws AuthorizeException, IOException, SQLException { Curator curator = new Curator(); curator.setReporter(reporter); - return curate(curator, c, wfi); + c.turnOffAuthorisationSystem(); + boolean wasAnonymous = false; + if (null == c.getCurrentUser()) { // We need someone to email + wasAnonymous = true; + c.setCurrentUser(ePersonService.getSystemEPerson(c)); + } + boolean failedP = curate(curator, c, wfi); + if (wasAnonymous) { + c.setCurrentUser(null); + } + c.restoreAuthSystemState(); + return failedP; } @Override @@ -130,7 +146,7 @@ public class XmlWorkflowCuratorServiceImpl curator.queue(c, item.getID().toString(), step.queue); } else { // Task is configured to be run automatically - curator.curate(item); + curator.curate(c, item); int status = curator.getStatus(task.name); String result = curator.getResult(task.name); String action = "none"; @@ -231,8 +247,12 @@ public class XmlWorkflowCuratorServiceImpl String status, String action, String message) throws AuthorizeException, IOException, SQLException { List epa = resolveContacts(c, task.getContacts(status), wfi); - if (epa.size() > 0) { + if (!epa.isEmpty()) { workflowService.notifyOfCuration(c, wfi, epa, task.name, action, message); + } else { + LOG.warn("No contacts were found for workflow item {}: " + + "task {} returned action {} with message {}", + wfi.getID(), task.name, action, message); } } @@ -255,8 +275,7 @@ public class XmlWorkflowCuratorServiceImpl // decode contacts if ("$flowgroup".equals(contact)) { // special literal for current flowgoup - ClaimedTask claimedTask = claimedTaskService.findByWorkflowIdAndEPerson(c, wfi, c.getCurrentUser()); - String stepID = claimedTask.getStepID(); + String stepID = getFlowStep(c, wfi).step; Step step; try { Workflow workflow = workflowFactory.getWorkflow(wfi.getCollection()); @@ -266,19 +285,26 @@ public class XmlWorkflowCuratorServiceImpl String.valueOf(wfi.getID()), e); return epList; } - RoleMembers roleMembers = step.getRole().getMembers(c, wfi); - for (EPerson ep : roleMembers.getEPersons()) { - epList.add(ep); - } - for (Group group : roleMembers.getGroups()) { - epList.addAll(group.getMembers()); + Role role = step.getRole(); + if (null != role) { + RoleMembers roleMembers = role.getMembers(c, wfi); + for (EPerson ep : roleMembers.getEPersons()) { + epList.add(ep); + } + for (Group group : roleMembers.getGroups()) { + epList.addAll(group.getMembers()); + } + } else { + epList.add(ePersonService.getSystemEPerson(c)); } } else if ("$colladmin".equals(contact)) { + // special literal for collection administrators Group adGroup = wfi.getCollection().getAdministrators(); if (adGroup != null) { epList.addAll(groupService.allMembers(c, adGroup)); } } else if ("$siteadmin".equals(contact)) { + // special literal for site administrator EPerson siteEp = ePersonService.findByEmail(c, configurationService.getProperty("mail.admin")); if (siteEp != null) { diff --git a/dspace-api/src/main/java/org/dspace/curate/service/XmlWorkflowCuratorService.java b/dspace-api/src/main/java/org/dspace/curate/service/XmlWorkflowCuratorService.java index 2ad1eac129..778b779cfe 100644 --- a/dspace-api/src/main/java/org/dspace/curate/service/XmlWorkflowCuratorService.java +++ b/dspace-api/src/main/java/org/dspace/curate/service/XmlWorkflowCuratorService.java @@ -42,9 +42,9 @@ public interface XmlWorkflowCuratorService { * * @param c the context * @param wfi the workflow item - * @return true if curation was completed or not required, + * @return true if curation was completed or not required; * false if tasks were queued for later completion, - * or item was rejected + * or item was rejected. * @throws AuthorizeException if authorization error * @throws IOException if IO error * @throws SQLException if database error @@ -58,7 +58,9 @@ public interface XmlWorkflowCuratorService { * @param curator the curation context * @param c the user context * @param wfId the workflow item's ID - * @return true if curation failed. + * @return true if curation curation was completed or not required; + * false if tasks were queued for later completion, + * or item was rejected. * @throws AuthorizeException if authorization error * @throws IOException if IO error * @throws SQLException if database error @@ -72,7 +74,9 @@ public interface XmlWorkflowCuratorService { * @param curator the curation context * @param c the user context * @param wfi the workflow item - * @return true if curation failed. + * @return true if workflow curation was completed or not required; + * false if tasks were queued for later completion, + * or item was rejected. * @throws AuthorizeException if authorization error * @throws IOException if IO error * @throws SQLException if database error diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java index 61477995c7..2d0574a630 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java @@ -47,6 +47,7 @@ import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.SubscribeService; import org.dspace.event.Event; import org.dspace.orcid.service.OrcidTokenService; +import org.dspace.services.ConfigurationService; import org.dspace.util.UUIDUtils; import org.dspace.versioning.Version; import org.dspace.versioning.VersionHistory; @@ -101,6 +102,8 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl impleme protected VersionDAO versionDAO; @Autowired(required = true) protected ClaimedTaskService claimedTaskService; + @Autowired(required = true) + protected ConfigurationService configurationService; @Autowired protected OrcidTokenService orcidTokenService; @@ -113,6 +116,30 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl impleme return ePersonDAO.findByID(context, EPerson.class, id); } + /** + * Create a fake EPerson which can receive email. Its address will be the + * value of "mail.admin", or "postmaster" if all else fails. + * @param c + * @return + * @throws SQLException + */ + @Override + public EPerson getSystemEPerson(Context c) + throws SQLException { + String adminEmail = configurationService.getProperty("mail.admin"); + if (null == adminEmail) { + adminEmail = "postmaster"; // Last-ditch attempt to send *somewhere* + } + EPerson systemEPerson = findByEmail(c, adminEmail); + + if (null == systemEPerson) { + systemEPerson = new EPerson(); + systemEPerson.setEmail(adminEmail); + } + + return systemEPerson; + } + @Override public EPerson findByIdOrLegacyId(Context context, String id) throws SQLException { if (StringUtils.isNumeric(id)) { diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/EPersonService.java b/dspace-api/src/main/java/org/dspace/eperson/service/EPersonService.java index c5c9801c16..47be942e97 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/EPersonService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/EPersonService.java @@ -13,6 +13,7 @@ import java.sql.SQLException; import java.util.Date; import java.util.List; import java.util.Set; +import javax.validation.constraints.NotNull; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Item; @@ -157,6 +158,19 @@ public interface EPersonService extends DSpaceObjectService, DSpaceObje public List findAll(Context context, int sortField, int pageSize, int offset) throws SQLException; + /** + * The "System EPerson" is a fake account that exists only to receive email. + * It has an email address that should be presumed usable. It does not + * exist in the database and is not complete. + * + * @param context current DSpace session. + * @return an EPerson that can presumably receive email. + * @throws SQLException + */ + @NotNull + public EPerson getSystemEPerson(Context context) + throws SQLException; + /** * Create a new eperson * diff --git a/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleDAOImpl.java b/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleDAOImpl.java index 3bd702bf80..71bb798ae3 100644 --- a/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/handle/dao/impl/HandleDAOImpl.java @@ -90,13 +90,11 @@ public class HandleDAOImpl extends AbstractHibernateDAO implements Handl @Override public long countHandlesByPrefix(Context context, String prefix) throws SQLException { - - CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Long.class); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Handle.class); Root handleRoot = criteriaQuery.from(Handle.class); - criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(Handle.class))); + criteriaQuery.select(handleRoot); criteriaQuery.where(criteriaBuilder.like(handleRoot.get(Handle_.handle), prefix + "%")); return countLong(context, criteriaQuery, criteriaBuilder, handleRoot); } diff --git a/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java index 59a1e13a21..82358362da 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/HandleIdentifierProvider.java @@ -68,10 +68,9 @@ public class HandleIdentifierProvider extends IdentifierProvider { try { String id = mint(context, dso); - // move canonical to point the latest version + // Populate metadata if (dso instanceof Item || dso instanceof Collection || dso instanceof Community) { - Item item = (Item) dso; - populateHandleMetadata(context, item, id); + populateHandleMetadata(context, dso, id); } return id; diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java index e6a092c472..9993f78b4d 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java @@ -95,11 +95,11 @@ public class VersionedHandleIdentifierProviderWithCanonicalHandles extends Ident String id = mint(context, dso); // move canonical to point the latest version - if (dso != null && dso.getType() == Constants.ITEM) { + if (dso.getType() == Constants.ITEM && dso instanceof Item) { Item item = (Item) dso; - VersionHistory history = null; + VersionHistory history; try { - history = versionHistoryService.findByItem(context, (Item) dso); + history = versionHistoryService.findByItem(context, item); } catch (SQLException ex) { throw new RuntimeException("A problem with the database connection occured.", ex); } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java index da7910da29..bc91a1fd92 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java @@ -221,6 +221,8 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService { //Get our next step, if none is found, archive our item firstStep = wf.getNextStep(context, wfi, firstStep, ActionResult.OUTCOME_COMPLETE); if (firstStep == null) { + // record the submitted provenance message + recordStart(context, wfi.getItem(),null); archive(context, wfi); } else { activateFirstStep(context, wf, firstStep, wfi); @@ -1187,25 +1189,30 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService { DCDate now = DCDate.getCurrent(); // Create provenance description - String provmessage = ""; + StringBuffer provmessage = new StringBuffer(); if (myitem.getSubmitter() != null) { - provmessage = "Submitted by " + myitem.getSubmitter().getFullName() - + " (" + myitem.getSubmitter().getEmail() + ") on " - + now.toString() + " workflow start=" + action.getProvenanceStartId() + "\n"; + provmessage.append("Submitted by ").append(myitem.getSubmitter().getFullName()) + .append(" (").append(myitem.getSubmitter().getEmail()).append(") on ") + .append(now.toString()); } else { // else, null submitter - provmessage = "Submitted by unknown (probably automated) on" - + now.toString() + " workflow start=" + action.getProvenanceStartId() + "\n"; + provmessage.append("Submitted by unknown (probably automated) on") + .append(now.toString()); + } + if (action != null) { + provmessage.append(" workflow start=").append(action.getProvenanceStartId()).append("\n"); + } else { + provmessage.append("\n"); } // add sizes and checksums of bitstreams - provmessage += installItemService.getBitstreamProvenanceMessage(context, myitem); + provmessage.append(installItemService.getBitstreamProvenanceMessage(context, myitem)); // Add message to the DC itemService .addMetadata(context, myitem, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", provmessage); + "description", "provenance", "en", provmessage.toString()); itemService.update(context, myitem); } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.6_2023.04.19__process_parameters_to_text_type.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.6_2023.04.19__process_parameters_to_text_type.sql deleted file mode 100644 index 6b2dd705ea..0000000000 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.6_2023.04.19__process_parameters_to_text_type.sql +++ /dev/null @@ -1,9 +0,0 @@ --- --- The contents of this file are subject to the license and copyright --- detailed in the LICENSE and NOTICE files at the root of the source --- tree and available online at --- --- http://www.dspace.org/license/ --- - -ALTER TABLE process MODIFY (parameters CLOB); diff --git a/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java b/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java index b9bca5e913..51291ee985 100644 --- a/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java +++ b/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java @@ -274,6 +274,8 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest { context.restoreAuthSystemState(); String status = helper.getAccessStatusFromItem(context, itemWithEmbargo, threshold); assertThat("testWithEmbargo 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO)); + String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); + assertThat("testWithEmbargo 1", embargoDate, equalTo(policy.getStartDate().toString())); } /** @@ -391,6 +393,8 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest { context.restoreAuthSystemState(); String status = helper.getAccessStatusFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold); assertThat("testWithPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO)); + String embargoDate = helper.getEmbargoFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold); + assertThat("testWithPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(policy.getStartDate().toString())); } /** @@ -420,6 +424,8 @@ public class DefaultAccessStatusHelperTest extends AbstractUnitTest { context.restoreAuthSystemState(); String status = helper.getAccessStatusFromItem(context, itemWithoutPrimaryAndMultipleBitstreams, threshold); assertThat("testWithNoPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); + String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); + assertThat("testWithNoPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(null)); } /** diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java index e40577ef36..25eb036159 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java @@ -26,6 +26,8 @@ import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.app.requestitem.RequestItem; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -37,6 +39,7 @@ import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.builder.RequestItemBuilder; import org.dspace.builder.ResourcePolicyBuilder; import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.EntityType; @@ -48,6 +51,8 @@ import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Constants; import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; import org.dspace.versioning.Version; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersioningService; @@ -68,6 +73,8 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase { protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); protected VersioningService versioningService = VersionServiceFactory.getInstance().getVersionService(); + protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); Community community; Collection collection1; @@ -752,6 +759,154 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase { assertNull(itemService.find(context, item.getID())); } + + @Test + public void testMoveItemToCollectionWithMoreRestrictiveItemReadPolicy() throws Exception { + /* Verify that, if we move an item from a collection with a permissive default item READ policy + * to a collection with a restrictive default item READ policy, + * that the item and its bundles do not retain the original permissive item READ policy. + * However, its bitstreams do. + */ + + context.turnOffAuthorisationSystem(); + + Group anonymous = groupService.findByName(context, Group.ANONYMOUS); + Group admin = groupService.findByName(context, Group.ADMIN); + + // Set up the two different collections: one permissive and one restrictive in its default READ policy. + Collection permissive = CollectionBuilder + .createCollection(context, community) + .build(); + Collection restrictive = CollectionBuilder + .createCollection(context, community) + .build(); + authorizeService.removePoliciesActionFilter(context, restrictive, Constants.DEFAULT_ITEM_READ); + authorizeService.addPolicy(context, restrictive, Constants.DEFAULT_ITEM_READ, admin); + + // Add an item to the permissive collection. + Item item = ItemBuilder + .createItem(context, permissive) + .build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, InputStream.nullInputStream()) + .build(); + + Bundle bundle = item.getBundles("ORIGINAL").get(0); + + // Verify that the item, bundle and bitstream each have exactly one READ policy, for the anonymous group. + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, item, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bundle, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bitstream, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + + // Move the item to the restrictive collection, making sure to inherit default policies. + itemService.move(context, item, permissive, restrictive, true); + + // Verify that the item's read policy now only allows administrators. + assertEquals( + List.of(admin), + authorizeService.getPoliciesActionFilter(context, item, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(admin), + authorizeService.getPoliciesActionFilter(context, bundle, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bitstream, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + + context.restoreAuthSystemState(); + } + + @Test + public void testMoveItemToCollectionWithMoreRestrictiveBitstreamReadPolicy() throws Exception { + /* Verify that, if we move an item from a collection with a permissive default bitstream READ policy + * to a collection with a restrictive default bitstream READ policy, + * that the item's bitstreams do not retain the original permissive READ policy. + * However, the item itself and its bundles do retain the original policy. + */ + + context.turnOffAuthorisationSystem(); + + Group anonymous = groupService.findByName(context, Group.ANONYMOUS); + Group admin = groupService.findByName(context, Group.ADMIN); + + // Set up the two different collections: one permissive and one restrictive in its default READ policy. + Collection permissive = CollectionBuilder + .createCollection(context, community) + .build(); + Collection restrictive = CollectionBuilder + .createCollection(context, community) + .build(); + authorizeService.removePoliciesActionFilter(context, restrictive, Constants.DEFAULT_BITSTREAM_READ); + authorizeService.addPolicy(context, restrictive, Constants.DEFAULT_BITSTREAM_READ, admin); + + // Add an item to the permissive collection. + Item item = ItemBuilder + .createItem(context, permissive) + .build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, InputStream.nullInputStream()) + .build(); + + Bundle bundle = item.getBundles("ORIGINAL").get(0); + + // Verify that the item, bundle and bitstream each have exactly one READ policy, for the anonymous group. + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, item, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bundle, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bitstream, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + + // Move the item to the restrictive collection, making sure to inherit default policies. + itemService.move(context, item, permissive, restrictive, true); + + // Verify that the bundle and bitstream's read policies now only allows administrators. + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, item, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(anonymous), + authorizeService.getPoliciesActionFilter(context, bundle, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + assertEquals( + List.of(admin), + authorizeService.getPoliciesActionFilter(context, bitstream, Constants.READ) + .stream().map(ResourcePolicy::getGroup).collect(Collectors.toList()) + ); + + context.restoreAuthSystemState(); + + } + private void assertMetadataValue(String authorQualifier, String contributorElement, String dcSchema, String value, String authority, int place, MetadataValue metadataValue) { assertThat(metadataValue.getValue(), equalTo(value)); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java index e27a3ee947..4f842b8e94 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java @@ -85,7 +85,6 @@ public class XOAI { // needed because the solr query only returns 10 rows by default private final Context context; - private boolean optimize; private final boolean verbose; private boolean clean; @@ -122,9 +121,8 @@ public class XOAI { return formats; } - public XOAI(Context context, boolean optimize, boolean clean, boolean verbose) { + public XOAI(Context context, boolean clean, boolean verbose) { this.context = context; - this.optimize = optimize; this.clean = clean; this.verbose = verbose; @@ -173,12 +171,6 @@ public class XOAI { } solrServerResolver.getServer().commit(); - if (optimize) { - println("Optimizing Index"); - solrServerResolver.getServer().optimize(); - println("Index optimized"); - } - // Set last compilation date xoaiLastCompilationCacheService.put(new Date()); return result; @@ -586,7 +578,6 @@ public class XOAI { CommandLineParser parser = new DefaultParser(); Options options = new Options(); options.addOption("c", "clear", false, "Clear index before indexing"); - options.addOption("o", "optimize", false, "Optimize index at the end"); options.addOption("v", "verbose", false, "Verbose output"); options.addOption("h", "help", false, "Shows some help"); options.addOption("n", "number", true, "FOR DEVELOPMENT MUST DELETE"); @@ -620,7 +611,7 @@ public class XOAI { if (COMMAND_IMPORT.equals(command)) { ctx = new Context(Context.Mode.READ_ONLY); - XOAI indexer = new XOAI(ctx, line.hasOption('o'), line.hasOption('c'), line.hasOption('v')); + XOAI indexer = new XOAI(ctx, line.hasOption('c'), line.hasOption('v')); applicationContext.getAutowireCapableBeanFactory().autowireBean(indexer); @@ -706,7 +697,6 @@ public class XOAI { System.out.println(" " + COMMAND_IMPORT + " - To import DSpace items into OAI index and cache system"); System.out.println(" " + COMMAND_CLEAN_CACHE + " - Cleans the OAI cached responses"); System.out.println("> Parameters:"); - System.out.println(" -o Optimize index after indexing (" + COMMAND_IMPORT + " only)"); System.out.println(" -c Clear index (" + COMMAND_IMPORT + " only)"); System.out.println(" -v Verbose output"); System.out.println(" -h Shows this text"); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java index 6b3c5ded98..3201a02291 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java @@ -12,6 +12,7 @@ import java.util.List; import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; +import org.apache.commons.lang3.StringUtils; import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.service.AccessStatusService; import org.dspace.content.Item; @@ -31,6 +32,13 @@ import org.dspace.xoai.util.ItemUtils; * open.access * * + * OR + * + * + * embargo + * 2024-10-10 + * + * * } * * Returning Values are based on: @@ -46,9 +54,15 @@ public class AccessStatusElementItemCompilePlugin implements XOAIExtensionItemCo String accessStatusType; accessStatusType = accessStatusService.getAccessStatus(context, item); + String embargoFromItem = accessStatusService.getEmbargoFromItem(context, item); + Element accessStatus = ItemUtils.create("access-status"); accessStatus.getField().add(ItemUtils.createValue("value", accessStatusType)); + if (StringUtils.isNotEmpty(embargoFromItem)) { + accessStatus.getField().add(ItemUtils.createValue("embargo", embargoFromItem)); + } + Element others; List elements = metadata.getElement(); if (ItemUtils.getElement(elements, "others") != null) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java index 7c0694c52f..f45dbee66f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RequestItemRepository.java @@ -21,7 +21,6 @@ import javax.servlet.http.HttpServletRequest; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.validator.routines.EmailValidator; import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; @@ -48,7 +47,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; - +import org.springframework.web.util.HtmlUtils; /** * Component to expose item requests. * @@ -173,11 +172,11 @@ public class RequestItemRepository username = user.getFullName(); } else { // An anonymous session may provide a name. // Escape username to evade nasty XSS attempts - username = StringEscapeUtils.escapeHtml4(rir.getRequestName()); + username = HtmlUtils.htmlEscape(rir.getRequestName(),"UTF-8"); } // Requester's message text, escaped to evade nasty XSS attempts - String message = StringEscapeUtils.escapeHtml4(rir.getRequestMessage()); + String message = HtmlUtils.htmlEscape(rir.getRequestMessage(),"UTF-8"); // Create the request. String token; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index bc276557df..864acf1a56 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -1695,6 +1695,53 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest .andExpect(jsonPath("$.type", is("bitstream"))); } + @Test + public void thumbnailEndpointTestWithSpecialCharactersInFileName() throws Exception { + // Given an Item + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1").build(); + + Item item = ItemBuilder.createItem(context, col1) + .withTitle("Test item -- thumbnail") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .build(); + + Bundle originalBundle = BundleBuilder.createBundle(context, item) + .withName(Constants.DEFAULT_BUNDLE_NAME) + .build(); + Bundle thumbnailBundle = BundleBuilder.createBundle(context, item) + .withName("THUMBNAIL") + .build(); + + InputStream is = IOUtils.toInputStream("dummy", "utf-8"); + + // With an ORIGINAL Bitstream & matching THUMBNAIL Bitstream containing special characters in filenames + Bitstream bitstream = BitstreamBuilder.createBitstream(context, originalBundle, is) + .withName("test (2023) file.pdf") + .withMimeType("application/pdf") + .build(); + Bitstream thumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle, is) + .withName("test (2023) file.pdf.jpg") + .withMimeType("image/jpeg") + .build(); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + + getClient(tokenAdmin).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/thumbnail")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(thumbnail.getID().toString()))) + .andExpect(jsonPath("$.type", is("bitstream"))); + } + @Test public void thumbnailEndpointMultipleThumbnailsWithPrimaryBitstreamTest() throws Exception { // Given an Item diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 3a1d75eb56..16c63c9c1a 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -1432,6 +1432,18 @@ literature + + literature + + + literature + + + literature + + + literature + dataset diff --git a/dspace/config/crosswalks/oai/metadataFormats/uketd_dc.xsl b/dspace/config/crosswalks/oai/metadataFormats/uketd_dc.xsl index a3a4e66670..5c434e49ed 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/uketd_dc.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/uketd_dc.xsl @@ -115,6 +115,20 @@ + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index aeb15fbefa..6feb81c884 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ 2.3.8 1.1.1 - 9.4.52.v20230823 + 9.4.53.v20231009 2.20.0 2.0.29 1.19.0