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 7bef232f0450..bfb8b24f33e2 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 @@ -55,6 +55,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.SearchService; import org.dspace.discovery.SearchServiceException; @@ -111,6 +112,8 @@ public class BulkAccessControl extends DSpaceRunnable resPolicies = resourcePolicyService.find(c, o, type); + resourcePolicyDAO.deleteByDsoAndTypeAndAction(c, o, type, action); c.turnOffAuthorisationSystem(); contentServiceFactory.getDSpaceObjectService(o).updateLastModified(c, o); c.restoreAuthSystemState(); + + provenanceService.removeReadPolicies(c, o, resPolicies); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java index 2119959073f0..a3d2316d0d05 100644 --- a/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/DSpaceObjectServiceImpl.java @@ -34,6 +34,7 @@ import org.dspace.content.service.RelationshipService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.dspace.handle.service.HandleService; import org.dspace.identifier.service.IdentifierService; import org.dspace.utils.DSpace; @@ -67,6 +68,8 @@ public abstract class DSpaceObjectServiceImpl implements protected MetadataAuthorityService metadataAuthorityService; @Autowired(required = true) protected RelationshipService relationshipService; + @Autowired(required = true) + protected ProvenanceService provenanceService; public DSpaceObjectServiceImpl() { @@ -377,6 +380,7 @@ public void clearMetadata(Context context, T dso, String schema, String element, } } dso.setMetadataModified(); + provenanceService.removeMetadata(context, dso, schema, element, qualifier); } @Override 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 e135f614ec4f..fbdd7b97dcca 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -53,6 +53,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; +import org.dspace.core.ProvenanceService; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverResult; import org.dspace.discovery.SearchService; @@ -174,6 +175,10 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) ClarinMatomoBitstreamTracker matomoBitstreamTracker; + @Autowired(required = true) + private ProvenanceService provenanceService; + + protected ItemServiceImpl() { super(); } @@ -1134,6 +1139,7 @@ public void move(Context context, Item item, Collection from, Collection to, boo context.addEvent(new Event(Event.MODIFY, Constants.ITEM, item.getID(), null, getIdentifiers(context, item))); } + provenanceService.moveItem(context, item, from); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java index dbe842a4194f..2e2798f49e84 100644 --- a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java +++ b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java @@ -34,6 +34,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.SiteService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.ProvenanceService; import org.dspace.eperson.service.SubscribeService; import org.dspace.handle.service.HandleClarinService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -77,6 +78,7 @@ public abstract class ContentServiceFactory { public abstract SiteService getSiteService(); public abstract SubscribeService getSubscribeService(); + public abstract PreviewContentService getPreviewContentService(); /** @@ -123,6 +125,13 @@ public abstract class ContentServiceFactory { */ public abstract HandleClarinService getHandleClarinService(); + /** + * Return the implementation of the ProvenanceService interface + * + * @return the ProvenanceService + */ + public abstract ProvenanceService getProvenanceService(); + public InProgressSubmissionService getInProgressSubmissionService(InProgressSubmission inProgressSubmission) { if (inProgressSubmission instanceof WorkspaceItem) { return getWorkspaceItemService(); diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java index a38dec0c0a9d..7b340cafd0c9 100644 --- a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java @@ -31,6 +31,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.SiteService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.ProvenanceService; import org.dspace.eperson.service.SubscribeService; import org.dspace.handle.service.HandleClarinService; import org.springframework.beans.factory.annotation.Autowired; @@ -93,6 +94,9 @@ public class ContentServiceFactoryImpl extends ContentServiceFactory { @Autowired(required = true) private HandleClarinService handleClarinService; + @Autowired(required = true) + private ProvenanceService provenanceService; + @Override public List> getDSpaceObjectServices() { return dSpaceObjectServices; @@ -173,6 +177,11 @@ public PreviewContentService getPreviewContentService() { return previewContentService; } + @Override + public ProvenanceService getProvenanceService() { + return provenanceService; + } + @Override public RelationshipTypeService getRelationshipTypeService() { return relationshipTypeService; diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java new file mode 100644 index 000000000000..a6f486b34c99 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageFormatter.java @@ -0,0 +1,104 @@ +/** + * 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.core; + +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.DCDate; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.InstallItemService; +import org.dspace.eperson.EPerson; + +/** + * The ProvenanceMessageProvider providing methods to generate provenance messages for DSpace items. + * It loads message templates + * from a JSON file and formats messages based on the context, including user details and timestamps. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public class ProvenanceMessageFormatter { + private InstallItemService installItemService; + + public ProvenanceMessageFormatter() {} + + public String getMessage(Context context, String messageTemplate, Item item, Object... args) + throws SQLException, AuthorizeException { + // Initialize InstallItemService if it is not initialized. + if (installItemService == null) { + installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + } + String msg = getMessage(context, messageTemplate, args); + msg = msg + "\n" + installItemService.getBitstreamProvenanceMessage(context, item); + return msg; + } + + public String getMessage(Context context, String messageTemplate, Object... args) { + EPerson currentUser = context.getCurrentUser(); + String timestamp = DCDate.getCurrent().toString(); + String details = validateMessageTemplate(messageTemplate, args); + return String.format("%s by %s (%s) on %s", + details, + currentUser.getFullName(), + currentUser.getEmail(), + timestamp); + } + + public String getMessage(Item item) { + String msg = "Item was in collections:\n"; + List collsList = item.getCollections(); + for (Collection coll : collsList) { + msg = msg + coll.getName() + " (ID: " + coll.getID() + ")\n"; + } + return msg; + } + + public String getMessage(Bitstream bitstream) { + // values of deleted bitstream + String msg = bitstream.getName() + ": " + + bitstream.getSizeBytes() + " bytes, checksum: " + + bitstream.getChecksum() + " (" + + bitstream.getChecksumAlgorithm() + ")\n"; + return msg; + } + + public String getMessage(List resPolicies) { + return resPolicies.stream() + .filter(rp -> rp.getAction() == Constants.READ) + .map(rp -> String.format("[%s, %s, %d, %s, %s, %s, %s]", + rp.getRpName(), rp.getRpType(), rp.getAction(), + rp.getEPerson() != null ? rp.getEPerson().getEmail() : null, + rp.getGroup() != null ? rp.getGroup().getName() : null, + rp.getStartDate() != null ? rp.getStartDate().toString() : null, + rp.getEndDate() != null ? rp.getEndDate().toString() : null)) + .collect(Collectors.joining(";")); + } + + public String getMetadata(String oldMtdKey, String oldMtdValue) { + return oldMtdKey + ": " + oldMtdValue; + } + + public String getMetadataField(MetadataField metadataField) { + return metadataField.toString() + .replace('_', '.'); + } + + private String validateMessageTemplate(String messageTemplate, Object... args) { + if (messageTemplate == null) { + throw new IllegalArgumentException("The provenance message template is null!"); + } + return String.format(messageTemplate, args); + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java new file mode 100644 index 000000000000..c14d3f447591 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageTemplates.java @@ -0,0 +1,38 @@ +/** + * 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.core; + +/** + * The ProvenanceMessageTemplates enum provides message templates for provenance messages. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public enum ProvenanceMessageTemplates { + ACCESS_CONDITION("Access condition (%s) was added to %s (%s)"), + RESOURCE_POLICIES_REMOVED("Resource policies (%s) of %s (%s) were removed"), + BUNDLE_ADDED("Item was added bitstream to bundle (%s)"), + EDIT_LICENSE("License (%s) was %s"), + MOVE_ITEM("Item was moved from collection (%s) to different collection"), + MAPPED_ITEM("Item was mapped to collection (%s)"), + DELETED_ITEM_FROM_MAPPED("Item was deleted from mapped collection (%s)"), + EDIT_BITSTREAM("Item (%s) was deleted bitstream (%s)"), + ITEM_METADATA("Item metadata (%s) was %s"), + BITSTREAM_METADATA("Item metadata (%s) was %s bitstream (%s)"), + ITEM_REPLACE_SINGLE_METADATA("Item bitstream (%s) metadata (%s) was updated"), + DISCOVERABLE("Item was made %sdiscoverable"); + + private final String template; + + ProvenanceMessageTemplates(String template) { + this.template = template; + } + + public String getTemplate() { + return template; + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java new file mode 100644 index 000000000000..9f34d89f9214 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java @@ -0,0 +1,177 @@ +/** + * 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.core; + +import java.util.List; + +import org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; + +/** + * The ProvenanceService is responsible for creating provenance metadata for items based on the actions performed. + * + * @author Milan Majchrak (dspace at dataquest.sk) + */ +public interface ProvenanceService { + /** + * Add a provenance message to the item when a new access condition is added + * + * @param context DSpace context object + * @param item item to which the access condition is added + * @param accessControl the access control input + */ + void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl); + + /** + * Add a provenance message to the item when a read policy is removed + * + * @param context DSpace context object + * @param dso DSpace object from which the read policy is removed + * @param resPolicies list of resource policies that are removed + */ + void removeReadPolicies(Context context, DSpaceObject dso, List resPolicies); + + /** + * Add a provenance message to the item when a bitstream policy is set + * + * @param context DSpace context object + * @param bitstream bitstream to which the policy is set + * @param item item to which the bitstream belongs + * @param accessControl the access control input + */ + void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, + BulkAccessControlInput accessControl); + + /** + * Add a provenance message to the item when an item's license is edited + * + * @param context DSpace context object + * @param item item to which the license is edited + * @param newLicense true if the license is new, false if it's edited + */ + void updateLicense(Context context, Item item, boolean newLicense); + + /** + * Add a provenance message to the item when it's moved to a collection + * + * @param context DSpace context object + * @param item item that is moved + * @param collection collection to which the item is moved + */ + void moveItem(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's mapped to a collection + * + * @param context DSpace context object + * @param item item that is mapped + * @param collection collection to which the item is mapped + */ + void mappedItem(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's deleted from a mapped collection + * + * @param context DSpace context object + * @param item item that is deleted from a mapped collection + * @param collection collection from which the item is deleted + */ + void deletedItemFromMapped(Context context, Item item, Collection collection); + + /** + * Add a provenance message to the item when it's bitstream is deleted + * + * @param context DSpace context object + * @param bitstream deleted bitstream + * @param item item from which the bitstream is deleted + */ + void deleteBitstream(Context context, Bitstream bitstream, Item item); + + /** + * Add a provenance message to the item when metadata is added + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is added + * @param metadataField metadata field that is added + */ + void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField); + + /** + * Add a provenance message to the item when metadata is removed + * + * @param context DSpace context object + * @param dso DSpace object from which the metadata is removed + */ + void removeMetadata(Context context, DSpaceObject dso, String schema, String element, String qualifier); + + /** + * Add a provenance message to the item when metadata is removed at a given index + * + * @param context DSpace context object + * @param dso DSpace object from which the metadata is removed + * @param metadataValues list of metadata values + * @param indexInt index at which the metadata is removed + */ + void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt); + + /** + * Add a provenance message to the item when metadata is replaced + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is replaced + * @param metadataField metadata field that is replaced + * @param oldMtdVal old metadata value + */ + void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal); + + /** + * Add a provenance message to the item when metadata is replaced + * + * @param context DSpace context object + * @param dso DSpace object to which the metadata is replaced + * @param metadataField metadata field that is replaced + * @param oldMtdVal old metadata value + */ + void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, + String oldMtdVal); + + /** + * Add a provenance message to the item when metadata is updated + * + * @param context DSpace context object + * @param item item to which the metadata is updated + * @param discoverable true if the item is discoverable, false if it's not + */ + void makeDiscoverable(Context context, Item item, boolean discoverable); + + /** + * Add a provenance message to the item when a bitstream is uploaded + * + * @param context DSpace context object + * @param bundle bundle to which the bitstream is uploaded + */ + void uploadBitstream(Context context, Bundle bundle); + + /** + * Fetch an Item object using a service and return the first Item object from the list. + * Log an error if the list is empty or if there is an SQL error + * + * @param context DSpace context object + * @param bitstream bitstream to which the item is fetched + */ + Item findItemByBitstream(Context context, Bitstream bitstream); + +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java new file mode 100644 index 000000000000..71b7128e5565 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java @@ -0,0 +1,353 @@ +/** + * 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.core; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.bulkaccesscontrol.model.AccessCondition; +import org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchemaEnum; +import org.dspace.content.MetadataValue; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinItemService; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * ProvenanceServiceImpl is an implementation of ProvenanceService. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public class ProvenanceServiceImpl implements ProvenanceService { + private static final Logger log = LogManager.getLogger(ProvenanceServiceImpl.class); + + @Autowired + private ItemService itemService; + @Autowired + private ClarinItemService clarinItemService; + @Autowired + private ClarinLicenseResourceMappingService clarinResourceMappingService; + @Autowired + private BitstreamService bitstreamService; + + private final ProvenanceMessageFormatter messageProvider = new ProvenanceMessageFormatter(); + + public void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl) { + String resPoliciesStr = extractAccessConditions(accessControl.getItem().getAccessConditions()); + if (StringUtils.isNotBlank(resPoliciesStr)) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ACCESS_CONDITION.getTemplate(), + resPoliciesStr, "item", item.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when setting item policies.", e); + } + } + } + + public void removeReadPolicies(Context context, DSpaceObject dso, List resPolicies) { + if (resPolicies.isEmpty()) { + return; + } + String resPoliciesStr = messageProvider.getMessage(resPolicies); + try { + if (dso.getType() == Constants.ITEM) { + Item item = (Item) dso; + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICIES_REMOVED.getTemplate(), + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "item", item.getID()); + addProvenanceMetadata(context, item, msg); + } else if (dso.getType() == Constants.BITSTREAM) { + Bitstream bitstream = (Bitstream) dso; + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.RESOURCE_POLICIES_REMOVED.getTemplate(), + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "bitstream", bitstream.getID()); + addProvenanceMetadata(context, item, msg); + } + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to remove read policies from the DSpace object.", e); + } + } + + public void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, + BulkAccessControlInput accessControl) { + String accConditionsStr = extractAccessConditions(accessControl.getBitstream().getAccessConditions()); + if (StringUtils.isNotBlank(accConditionsStr)) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ACCESS_CONDITION.getTemplate(), + accConditionsStr, "bitstream", bitstream.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when setting bitstream policies.", e); + } + } + } + + public void updateLicense(Context context, Item item, boolean newLicense) { + String oldLicense = null; + + try { + oldLicense = findLicenseInBundles(item, Constants.LICENSE_BUNDLE_NAME, oldLicense, context); + if (oldLicense == null) { + oldLicense = findLicenseInBundles(item, Constants.CONTENT_BUNDLE_NAME, oldLicense, context); + } + + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.EDIT_LICENSE.getTemplate(), + item, Objects.isNull(oldLicense) ? "empty" : oldLicense, + !newLicense ? "removed" : Objects.isNull(oldLicense) ? "added" : "updated"); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when editing Item's license.", e); + } + + } + + public void moveItem(Context context, Item item, Collection collection) { + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.MOVE_ITEM.getTemplate(), + item, collection.getID()); + // Update item in DB + // Because a user can move an item without authorization turn off authorization + context.turnOffAuthorisationSystem(); + addProvenanceMetadata(context, item, msg); + context.restoreAuthSystemState(); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when moving an item to a different collection.", + e); + } + } + + public void mappedItem(Context context, Item item, Collection collection) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.MAPPED_ITEM.getTemplate(), + collection.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when mapping an item into a collection.", e); + } + } + + public void deletedItemFromMapped(Context context, Item item, Collection collection) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.DELETED_ITEM_FROM_MAPPED.getTemplate(), collection.getID()); + try { + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when deleting an item from a mapped collection.", + e); + } + } + + public void deleteBitstream(Context context, Bitstream bitstream, Item item) { + try { + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.EDIT_BITSTREAM.getTemplate(), item, item.getID(), + messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when deleting a bitstream.", e); + } + } + + public void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField) { + try { + if (Constants.ITEM == dso.getType()) { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + messageProvider.getMetadataField(metadataField), "added"); + addProvenanceMetadata(context, (Item) dso, msg); + } + + if (dso.getType() == Constants.BITSTREAM) { + Bitstream bitstream = (Bitstream) dso; + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.BITSTREAM_METADATA.getTemplate(), item, + messageProvider.getMetadataField(metadataField), "added by", + messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when adding metadata to a DSpace object.", e); + } + } + + public void removeMetadata(Context context, DSpaceObject dso, String schema, String element, String qualifier) { + if (dso.getType() != Constants.BITSTREAM) { + return; + } + MetadataField oldMtdKey = null; + String oldMtdValue = null; + List mtd = bitstreamService.getMetadata((Bitstream) dso, schema, element, qualifier, Item.ANY); + if (CollectionUtils.isEmpty(mtd)) { + // Do not add any provenance message when there are no metadata to remove + return; + } + oldMtdKey = mtd.get(0).getMetadataField(); + oldMtdValue = mtd.get(0).getValue(); + Bitstream bitstream = (Bitstream) dso; + try { + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.BITSTREAM_METADATA.getTemplate(), item, + messageProvider.getMetadata(messageProvider.getMetadataField(oldMtdKey), oldMtdValue), + "deleted from", messageProvider.getMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when removing metadata from a dso.", e); + } + + } + + public void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt) { + if (dso.getType() != Constants.ITEM) { + return; + } + // Remember removed mtd + String oldMtdKey = messageProvider.getMetadataField(metadataValues.get(indexInt).getMetadataField()); + String oldMtdValue = metadataValues.get(indexInt).getValue(); + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + (Item) dso, messageProvider.getMetadata(oldMtdKey, oldMtdValue), "deleted"); + addProvenanceMetadata(context, (Item) dso, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when removing metadata at a specific index " + + "from a dso", e); + } + } + + public void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal) { + if (dso.getType() != Constants.ITEM) { + return; + } + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.ITEM_METADATA.getTemplate(), + (Item) dso,messageProvider.getMetadata(messageProvider.getMetadataField(metadataField), + oldMtdVal), "updated"); + addProvenanceMetadata(context, (Item) dso, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when replacing metadata in a dso.", e); + } + + } + + public void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, + String oldMtdVal) { + if (dso.getType() != Constants.BITSTREAM) { + return; + } + + Bitstream bitstream = (Bitstream) dso; + try { + Item item = findItemByBitstream(context, bitstream); + if (Objects.nonNull(item)) { + String msg = messageProvider.getMessage(context, + ProvenanceMessageTemplates.ITEM_REPLACE_SINGLE_METADATA.getTemplate(), item, + messageProvider.getMessage(bitstream), + messageProvider.getMetadata(messageProvider.getMetadataField(metadataField), oldMtdVal)); + addProvenanceMetadata(context, item, msg);; + } + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when replacing metadata in a item.", e); + } + } + + public void makeDiscoverable(Context context, Item item, boolean discoverable) { + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.DISCOVERABLE.getTemplate(), + item, discoverable ? "" : "non-") + messageProvider.getMessage(item); + addProvenanceMetadata(context, item, msg); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when making an item discoverable.", e); + } + } + + public void uploadBitstream(Context context, Bundle bundle) { + Item item = bundle.getItems().get(0); + try { + String msg = messageProvider.getMessage(context, ProvenanceMessageTemplates.BUNDLE_ADDED.getTemplate(), + item, bundle.getID()); + addProvenanceMetadata(context,item, msg); + itemService.update(context, item); + } catch (SQLException | AuthorizeException e) { + log.error("Unable to add new provenance metadata when updating an item's bitstream.", e); + } + } + + private void addProvenanceMetadata(Context context, Item item, String msg) + throws SQLException, AuthorizeException { + itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), + "description", "provenance", "en", msg); + itemService.update(context, item); + } + + private String extractAccessConditions(List accessConditions) { + return accessConditions.stream() + .map(AccessCondition::getName) + .collect(Collectors.joining(";")); + } + + public Item findItemByBitstream(Context context, Bitstream bitstream) { + List items = null; + try { + items = clarinItemService.findByBitstreamUUID(context, bitstream.getID()); + } catch (SQLException e) { + log.error("Unable to find item by bitstream (" + bitstream.getID() + " ).", e); + return null; + } + if (items.isEmpty()) { + log.warn("Bitstream (" + bitstream.getID() + ") is not assigned to any item."); + return null; + } + return items.get(0); + } + + private String findLicenseInBundles(Item item, String bundleName, String currentLicense, Context context) + throws SQLException { + List bundles = item.getBundles(bundleName); + for (Bundle clarinBundle : bundles) { + List bitstreamList = clarinBundle.getBitstreams(); + for (Bitstream bundleBitstream : bitstreamList) { + if (Objects.isNull(currentLicense)) { + List mappings = + this.clarinResourceMappingService.findByBitstreamUUID(context, bundleBitstream.getID()); + if (CollectionUtils.isNotEmpty(mappings)) { + return mappings.get(0).getLicense().getName(); + } + } + } + } + return currentLicense; + } +} diff --git a/dspace-api/src/test/java/org/dspace/builder/ClarinLicenseResourceMappingBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ClarinLicenseResourceMappingBuilder.java index 4a39a44fd4b1..c2bd6ab77831 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ClarinLicenseResourceMappingBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ClarinLicenseResourceMappingBuilder.java @@ -8,6 +8,7 @@ package org.dspace.builder; import java.sql.SQLException; +import java.util.Objects; import org.dspace.authorize.AuthorizeException; import org.dspace.content.clarin.ClarinLicenseResourceMapping; @@ -37,6 +38,20 @@ private ClarinLicenseResourceMappingBuilder create(final Context context) { return this; } + public static void delete(Integer id) throws Exception { + if (Objects.isNull(id)) { + return; + } + try (Context c = new Context()) { + ClarinLicenseResourceMapping clarinLicense = clarinLicenseResourceMappingService.find(c, id); + + if (clarinLicense != null) { + clarinLicenseResourceMappingService.delete(c, clarinLicense); + } + c.complete(); + } + } + @Override public void cleanup() throws Exception { try (Context c = new Context()) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemAddBundleController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemAddBundleController.java index b3444a739e77..74f28bbfd19c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemAddBundleController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemAddBundleController.java @@ -38,6 +38,7 @@ import org.dspace.content.service.clarin.ClarinLicenseService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -98,6 +99,9 @@ public class ItemAddBundleController { @Autowired ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; + @Autowired + ProvenanceService provenanceService; + /** * Method to add a Bundle to an Item with the given UUID in the URL. This will create a Bundle with the * name provided in the request and attach this to the Item that matches the UUID in the URL. @@ -183,6 +187,7 @@ public ItemRest updateLicenseForBundle(@PathVariable UUID uuid, } itemService.update(context, item); + provenanceService.updateLicense(context, item, !Objects.isNull(clarinLicense)); context.commit(); return converter.toRest(item, utils.obtainProjection()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java index b5a0c957f265..109b79c86111 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ItemOwningCollectionUpdateRestController.java @@ -32,6 +32,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.security.access.prepost.PostAuthorize; @@ -65,6 +66,9 @@ public class ItemOwningCollectionUpdateRestController { @Autowired Utils utils; + @Autowired + ProvenanceService provenanceService; + /** * This method will update the owning collection of the item that correspond to the provided item uuid, effectively * moving the item to the new collection. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/MappedCollectionRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/MappedCollectionRestController.java index 14dae21ebec0..09581f5e2998 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/MappedCollectionRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/MappedCollectionRestController.java @@ -30,6 +30,7 @@ import org.dspace.content.service.CollectionService; import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; @@ -57,6 +58,9 @@ public class MappedCollectionRestController { @Autowired Utils utils; + @Autowired + ProvenanceService provenanceService; + /** * This method will add an Item to a Collection. The Collection object is encapsulated in the request due to the * text/uri-list consumer and the Item UUID comes from the path in the URL @@ -105,6 +109,7 @@ public void createCollectionToItemRelation(@PathVariable UUID uuid, collectionService.addItem(context, collectionToMapTo, item); collectionService.update(context, collectionToMapTo); itemService.update(context, item); + provenanceService.mappedItem(context, item, collectionToMapTo); } else { throw new UnprocessableEntityException("Not a valid collection or item uuid."); } @@ -151,12 +156,12 @@ public void deleteCollectionToItemRelation(@PathVariable UUID uuid, @PathVariabl collectionService.removeItem(context, collection, item); collectionService.update(context, collection); itemService.update(context, item); + provenanceService.deletedItemFromMapped(context,item, collection); context.commit(); } } else { throw new UnprocessableEntityException("Not a valid collection or item uuid."); } - } private void checkIfItemIsTemplate(Item item) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleRestRepository.java index f750743db66e..7f800ea81813 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BundleRestRepository.java @@ -35,6 +35,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -69,6 +70,9 @@ public class BundleRestRepository extends DSpaceObjectRestRepository { BitstreamService bitstreamService; @Autowired AuthorizeService authorizeService; + @Autowired + ProvenanceService provenanceService; public static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; @Override @@ -53,9 +57,14 @@ public Bitstream perform(Context context, Bitstream resource, Operation operatio throw new RESTBitstreamNotFoundException(bitstreamIDtoDelete); } authorizeBitstreamRemoveAction(context, bitstreamToDelete, Constants.DELETE); - try { + // Find the item to which the bitstream belongs before deleting the bitstream, + // because after deletion, the item will no longer be connected to the bitstream. + Item item = provenanceService.findItemByBitstream(context, bitstreamToDelete); + // Delete the bitstream bitstreamService.delete(context, bitstreamToDelete); + // Update the provenance metadata after the bitstream has been successfully deleted + provenanceService.deleteBitstream(context, bitstreamToDelete, item); } catch (AuthorizeException | IOException e) { throw new RuntimeException(e.getMessage(), e); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataAddOperation.java index 4b27ae963ab0..4f343019942b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataAddOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataAddOperation.java @@ -17,6 +17,9 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -34,8 +37,11 @@ @Component public class DSpaceObjectMetadataAddOperation extends PatchOperation { + private static final Logger log = LoggerFactory.getLogger(DSpaceObjectMetadataAddOperation.class); @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; + @Autowired + ProvenanceService provenanceService; @Override public R perform(Context context, R resource, Operation operation) throws SQLException { @@ -69,9 +75,13 @@ private void add(Context context, DSpaceObject dso, DSpaceObjectService dsoServi dsoService.addAndShiftRightMetadata(context, dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(), metadataField.getQualifier(), metadataValue.getLanguage(), metadataValue.getValue(), metadataValue.getAuthority(), metadataValue.getConfidence(), indexInt); + provenanceService.addMetadata(context, dso, metadataField); } catch (SQLException e) { - throw new DSpaceBadRequestException("SQLException in DspaceObjectMetadataAddOperation.add trying to add " + - "metadata to dso.", e); + String msg; + msg = "SQLException in DspaceObjectMetadataAddOperation.add trying to add " + + "metadata to dso."; + log.error(msg, e); + throw new DSpaceBadRequestException(msg, e); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataRemoveOperation.java index 3164ae377aeb..2d09150233e3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataRemoveOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataRemoveOperation.java @@ -21,6 +21,9 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -41,8 +44,11 @@ @Component public class DSpaceObjectMetadataRemoveOperation extends PatchOperation { + private static final Logger log = LoggerFactory.getLogger(DSpaceObjectMetadataRemoveOperation.class); @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; + @Autowired + ProvenanceService provenanceService; @Override public R perform(Context context, R resource, Operation operation) throws SQLException { @@ -82,6 +88,7 @@ private void remove(Context context, DSpaceObject dso, DSpaceObjectService dsoSe // remove that metadata dsoService.removeMetadataValues(context, dso, Arrays.asList(metadataValues.get(indexInt))); + provenanceService.removeMetadataAtIndex(context, dso, metadataValues, indexInt); } else { throw new UnprocessableEntityException("UnprocessableEntityException - There is no metadata of " + "this type at that index"); @@ -91,9 +98,9 @@ private void remove(Context context, DSpaceObject dso, DSpaceObjectService dsoSe throw new IllegalArgumentException("This index (" + index + ") is not valid number.", e); } catch (ArrayIndexOutOfBoundsException e) { throw new UnprocessableEntityException("There is no metadata of this type at that index"); - } catch (SQLException e) { - throw new DSpaceBadRequestException("SQLException in DspaceObjectMetadataRemoveOperation.remove " + - "trying to remove metadata from dso.", e); + } catch (SQLException ex) { + throw new DSpaceBadRequestException("SQLException in DspaceObjectMetadataRemoveOperation.remove" + + " trying to remove metadata from dso.", ex); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java index 1cf15684587b..129c99a3c9cd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java @@ -21,6 +21,9 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -38,9 +41,11 @@ */ @Component public class DSpaceObjectMetadataReplaceOperation extends PatchOperation { - + private static final Logger log = LoggerFactory.getLogger(DSpaceObjectMetadataReplaceOperation.class); @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; + @Autowired + ProvenanceService provenanceService; @Override public R perform(Context context, R resource, Operation operation) throws SQLException { @@ -91,11 +96,12 @@ private void replace(Context context, DSpaceObject dso, DSpaceObjectService dsoS } // replace single existing metadata value if (propertyOfMd == null) { - this.replaceSingleMetadataValue(dso, dsoService, metadataField, metadataValue, index); + this.replaceSingleMetadataValue(context, dso, dsoService, metadataField, metadataValue, index); return; } // replace single property of exiting metadata value - this.replaceSinglePropertyOfMdValue(dso, dsoService, metadataField, index, propertyOfMd, valueMdProperty); + this.replaceSinglePropertyOfMdValue(context, dso, dsoService, metadataField, + index, propertyOfMd, valueMdProperty); } /** @@ -145,9 +151,10 @@ private void replaceMetadataFieldMetadata(Context context, DSpaceObject dso, DSp * @param index index of md being replaced */ // replace single existing metadata value - private void replaceSingleMetadataValue(DSpaceObject dso, DSpaceObjectService dsoService, + private void replaceSingleMetadataValue(Context context, DSpaceObject dso, DSpaceObjectService dsoService, MetadataField metadataField, MetadataValueRest metadataValue, String index) { + String msg; try { List metadataValues = dsoService.getMetadata(dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(), @@ -157,11 +164,13 @@ private void replaceSingleMetadataValue(DSpaceObject dso, DSpaceObjectService ds && metadataValues.get(indexInt) != null) { // Alter this existing md MetadataValue existingMdv = metadataValues.get(indexInt); + String oldMtdVal = existingMdv.getValue(); existingMdv.setAuthority(metadataValue.getAuthority()); existingMdv.setConfidence(metadataValue.getConfidence()); existingMdv.setLanguage(metadataValue.getLanguage()); existingMdv.setValue(metadataValue.getValue()); dsoService.setMetadataModified(dso); + provenanceService.replaceMetadata(context, dso, metadataField, oldMtdVal); } else { throw new UnprocessableEntityException("There is no metadata of this type at that index"); } @@ -179,7 +188,7 @@ private void replaceSingleMetadataValue(DSpaceObject dso, DSpaceObjectService ds * @param propertyOfMd property of md being replaced * @param valueMdProperty new value of property of md being replaced */ - private void replaceSinglePropertyOfMdValue(DSpaceObject dso, DSpaceObjectService dsoService, + private void replaceSinglePropertyOfMdValue(Context context, DSpaceObject dso, DSpaceObjectService dsoService, MetadataField metadataField, String index, String propertyOfMd, String valueMdProperty) { try { @@ -190,6 +199,8 @@ private void replaceSinglePropertyOfMdValue(DSpaceObject dso, DSpaceObjectServic if (indexInt >= 0 && metadataValues.size() > indexInt && metadataValues.get(indexInt) != null) { // Alter only asked propertyOfMd MetadataValue existingMdv = metadataValues.get(indexInt); + String oldMtdVal = existingMdv.getValue(); + if (propertyOfMd.equals("authority")) { existingMdv.setAuthority(valueMdProperty); } @@ -203,6 +214,7 @@ private void replaceSinglePropertyOfMdValue(DSpaceObject dso, DSpaceObjectServic existingMdv.setValue(valueMdProperty); } dsoService.setMetadataModified(dso); + provenanceService.replaceMetadataSingle(context, dso, metadataField, oldMtdVal); } else { throw new UnprocessableEntityException("There is no metadata of this type at that index"); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ItemDiscoverableReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ItemDiscoverableReplaceOperation.java index df17d4e92da3..22f136638c32 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ItemDiscoverableReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ItemDiscoverableReplaceOperation.java @@ -12,6 +12,8 @@ import org.dspace.app.rest.model.patch.Operation; import org.dspace.content.Item; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -31,6 +33,9 @@ public class ItemDiscoverableReplaceOperation extends PatchOperation { */ private static final String OPERATION_PATH_DISCOVERABLE = "/discoverable"; + @Autowired + ProvenanceService provenanceService; + @Override public R perform(Context context, R object, Operation operation) { checkOperationValue(operation.getValue()); @@ -41,6 +46,7 @@ public R perform(Context context, R object, Operation operation) { throw new UnprocessableEntityException("A template item cannot be discoverable."); } item.setDiscoverable(discoverable); + provenanceService.makeDiscoverable(context, item, discoverable); return object; } else { throw new DSpaceBadRequestException("ItemDiscoverableReplaceOperation does not support this operation"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 8f139a03f5d2..4e6337d33fef 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -2439,7 +2439,8 @@ private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Ex context.restoreAuthSystemState(); String token = getAuthToken(asUser.getEmail(), password); - new MetadataPatchSuite().runWith(getClient(token), "/api/core/items/" + item.getID(), expectedStatus); + new MetadataPatchSuite("item-metadata-patch-suite.json").runWith(getClient(token), + "/api/core/items/" + item.getID(), expectedStatus); } /** diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceExpectedMessages.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceExpectedMessages.java new file mode 100644 index 000000000000..d95412a20bfd --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceExpectedMessages.java @@ -0,0 +1,50 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +/** + * The ProvenanceExpectedMessages enum provides message templates for provenance messages. + * + * @author Michaela Paurikova (dspace at dataquest.sk) + */ +public enum ProvenanceExpectedMessages { + DISCOVERABLE("Item was made discoverable by first (admin) last (admin) (admin@email.com) on \nNo. " + + "of bitstreams: 0\nItem was in collections:\n"), + NON_DISCOVERABLE("Item was made non-discoverable by first (admin) last (admin) (admin@email.com) on " + + "\nNo. of bitstreams: 0\nItem was in collections:\n"), + MAPPED_COL("was mapped to collection"), + ADD_ITEM_MTD("Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on"), + REPLACE_ITEM_MTD("Item metadata (dc.title: Public item 1) was updated by first (admin) last (admin) " + + "(admin@email.com) on \nNo. of bitstreams: 0"), + REMOVE_ITEM_MTD("Item metadata (dc.title: Public item 1) was deleted by first (admin) last (admin) " + + "(admin@email.com) on \nNo. of bitstreams: 0"), + REMOVE_BITSTREAM_MTD("Item metadata (dc.description) was added by bitstream"), + REPLACE_BITSTREAM_MTD("metadata (dc.title: test) was updated by first (admin) last (admin) " + + "(admin@email.com) on \nNo. of bitstreams: 1\n"), + REMOVE_BITSTREAM("was deleted bitstream"), + ADD_BITSTREAM("Item was added bitstream to bundle"), + UPDATE_LICENSE("License (Test 1) was updated by first (admin) last (admin) (admin@email.com) " + + "on \nNo. of bitstreams: 1\n"), + ADD_LICENSE("License (empty) was added by first (admin) last (admin) (admin@email.com) on \nNo." + + " of bitstreams: 0"), + REMOVE_LICENSE("License (Test) was removed by first (admin) last (admin) (admin@email.com) on " + + "\nNo. of bitstreams: 1\n"), + MOVED_ITEM_COL("Item was moved from collection "); + + private final String template; + + // Constructor to initialize enum with the template string + ProvenanceExpectedMessages(String template) { + this.template = template; + } + + // Method to retrieve the template string + public String getTemplate() { + return template; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceServiceIT.java new file mode 100644 index 000000000000..d9aa3768e250 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceServiceIT.java @@ -0,0 +1,499 @@ +/** + * 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; + +import static java.nio.charset.Charset.defaultCharset; +import static org.apache.commons.io.IOUtils.toInputStream; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; +import static org.springframework.http.MediaType.parseMediaType; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.ws.rs.core.MediaType; + +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.RemoveOperation; +import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.BundleBuilder; +import org.dspace.builder.ClarinLicenseBuilder; +import org.dspace.builder.ClarinLicenseLabelBuilder; +import org.dspace.builder.ClarinLicenseResourceMappingBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.clarin.ClarinLicense; +import org.dspace.content.clarin.ClarinLicenseLabel; +import org.dspace.content.clarin.ClarinLicenseResourceMapping; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.clarin.ClarinLicenseLabelService; +import org.dspace.content.service.clarin.ClarinLicenseService; +import org.dspace.core.Constants; +import org.dspace.discovery.SearchServiceException; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +public class ProvenanceServiceIT extends AbstractControllerIntegrationTest { + @Autowired + private ItemService itemService; + @Autowired + private ClarinLicenseLabelService clarinLicenseLabelService; + @Autowired + private ClarinLicenseService clarinLicenseService; + + private Collection collection; + private Item item; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + collection = CollectionBuilder.createCollection(context, parentCommunity).build(); + item = ItemBuilder.createItem(context, collection) + .withTitle("Public item 1") + .build(); + context.restoreAuthSystemState(); + } + + @After + @Override + public void destroy() throws Exception { + context.turnOffAuthorisationSystem(); + // Delete community created in init() + try { + ItemBuilder.deleteItem(item.getID()); + CollectionBuilder.deleteCollection(collection.getID()); + CommunityBuilder.deleteCommunity(parentCommunity.getID()); + } catch (Exception e) { + // ignore + } + context.restoreAuthSystemState(); + + item = null; + collection = null; + parentCommunity = null; + super.destroy(); + } + + @Test + public void updateLicenseTest() throws Exception { + Bitstream bitstream = createBitstream(item, Constants.LICENSE_BUNDLE_NAME); + ClarinLicense clarinLicense1 = createClarinLicense("Test 1", "Test Def"); + ClarinLicenseResourceMapping mapping = createResourceMapping(clarinLicense1, bitstream); + ClarinLicense clarinLicense2 = createClarinLicense("Test 2", "Test Def"); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(put("/api/core/items/" + item.getID() + "/bundles") + .param("licenseID", clarinLicense2.getID().toString())) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.UPDATE_LICENSE.getTemplate()); + + deleteBitstream(bitstream); + deleteClarinLicense(clarinLicense1); + deleteClarinLicense(clarinLicense2); + deleteResourceMapping(mapping.getID()); + } + + @Test + public void addLicenseTest() throws Exception { + ClarinLicense clarinLicense = createClarinLicense("Test", "Test Def"); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(put("/api/core/items/" + item.getID() + "/bundles") + .param("licenseID", clarinLicense.getID().toString())) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.ADD_LICENSE.getTemplate()); + + deleteClarinLicense(clarinLicense); + } + + @Test + public void removeLicenseTest() throws Exception { + Bitstream bitstream = createBitstream(item, Constants.LICENSE_BUNDLE_NAME); + ClarinLicense clarinLicense = createClarinLicense("Test", "Test Def"); + ClarinLicenseResourceMapping mapping = createResourceMapping(clarinLicense, bitstream); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(put("/api/core/items/" + item.getID() + "/bundles") + .param("licenseID", "-1")) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.REMOVE_LICENSE.getTemplate()); + + deleteBitstream(bitstream); + deleteClarinLicense(clarinLicense); + deleteResourceMapping(mapping.getID()); + } + + @Test + public void makeDiscoverableTest() throws Exception { + item.setDiscoverable(false); + String token = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + ReplaceOperation replaceOperation = new ReplaceOperation("/discoverable", true); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + + getClient(token).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(item.getID().toString()))) + .andExpect(jsonPath("$.discoverable", Matchers.is(true))); + + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.DISCOVERABLE.getTemplate()); + } + + @Test + public void makeNonDiscoverableTest() throws Exception { + item.setDiscoverable(true); + String token = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + ReplaceOperation replaceOperation = new ReplaceOperation("/discoverable", false); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + + getClient(token).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.NON_DISCOVERABLE.getTemplate()); + } + + @Test + public void addedToMappedCollTest() throws Exception { + Collection coll = createCollection(); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform( + post("/api/core/items/" + item.getID() + "/mappedCollections/") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/spring-rest/api/core/collections/" + coll.getID() + "\n" + ) + ); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.MAPPED_COL.getTemplate()); + + deleteCollection(coll.getID()); + } + + @Test + public void addItemMetadataTest() throws Exception { + String adminToken = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + AddOperation addOperation = new AddOperation("/metadata/dc.title", "Test"); + ops.add(addOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.ADD_ITEM_MTD.getTemplate()); + } + + @Test + public void replaceItemMetadataTest() throws Exception { + String adminToken = getAuthToken(admin.getEmail(), password); + int index = 0; + List ops = new ArrayList<>(); + ReplaceOperation replaceOperation = new ReplaceOperation("/metadata/dc.title/" + index, "Test"); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.REPLACE_ITEM_MTD.getTemplate()); + } + + @Test + public void removeItemMetadataTest() throws Exception { + int index = 0; + String adminToken = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + RemoveOperation removeOperation = new RemoveOperation("/metadata/dc.title/" + index); + ops.add(removeOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.REMOVE_ITEM_MTD.getTemplate()); + } + + @Test + public void removeBitstreamMetadataTest() throws Exception { + Bitstream bitstream = createBitstream(item, "test"); + + String adminToken = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + AddOperation addOperation = new AddOperation("/metadata/dc.description", "test"); + ops.add(addOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/bitstreams/" + bitstream.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), + ProvenanceExpectedMessages.REMOVE_BITSTREAM_MTD.getTemplate()); + + deleteBitstream(bitstream); + } + + @Test + public void addBitstreamMetadataTest() throws Exception { + Bitstream bitstream = createBitstream(item, "test"); + + String adminToken = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + AddOperation addOperation = new AddOperation("/metadata/dc.description", "test"); + ops.add(addOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/bitstreams/" + bitstream.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), + ProvenanceExpectedMessages.REMOVE_BITSTREAM_MTD.getTemplate()); + } + + @Test + public void updateMetadataBitstreamTest() throws Exception { + Bitstream bitstream = createBitstream(item, "test"); + bitstream.setName(context, "test"); + + String adminToken = getAuthToken(admin.getEmail(), password); + int index = 0; + List ops = new ArrayList<>(); + ReplaceOperation replaceOperation = new ReplaceOperation("/metadata/dc.title/" + index + "/value", "test 1"); + ops.add(replaceOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/bitstreams/" + bitstream.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), + ProvenanceExpectedMessages.REPLACE_BITSTREAM_MTD.getTemplate()); + + deleteBitstream(bitstream); + } + + @Test + public void removeBitstreamFromItemTest() throws Exception { + Bitstream bitstream = createBitstream(item, "test"); + + String adminToken = getAuthToken(admin.getEmail(), password); + List ops = new ArrayList<>(); + RemoveOperation removeOperation = new RemoveOperation("/bitstreams/" + bitstream.getID()); + ops.add(removeOperation); + String patchBody = getPatchContent(ops); + getClient(adminToken).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.REMOVE_BITSTREAM.getTemplate()); + + deleteBitstream(bitstream); + } + + @Test + public void addBitstreamToItemTest() throws Exception { + Bundle bundle = createBundle(item, "test"); + + String token = getAuthToken(admin.getEmail(), password); + String input = "Hello, World!"; + context.turnOffAuthorisationSystem(); + MockMultipartFile file = new MockMultipartFile("file", "hello.txt", + org.springframework.http.MediaType.TEXT_PLAIN_VALUE, + input.getBytes()); + context.restoreAuthSystemState(); + getClient(token) + .perform(MockMvcRequestBuilders.multipart("/api/core/bundles/" + bundle.getID() + "/bitstreams") + .file(file)) + .andExpect(status().isCreated()); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.ADD_BITSTREAM.getTemplate()); + + deleteBundle(bundle.getID()); + } + + @Test + public void moveItemColTest() throws Exception { + Collection col = createCollection(); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token) + .perform(put("/api/core/items/" + item.getID() + "/owningCollection/") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content( + "https://localhost:8080/spring-rest/api/core/collections/" + col.getID() + )) + .andExpect(status().isOk()); + objectCheck(itemService.find(context, item.getID()), ProvenanceExpectedMessages.MOVED_ITEM_COL.getTemplate()); + + deleteCollection(col.getID()); + } + + + private String provenanceMetadataModified(String metadata) { + // Regex to match the date pattern + String datePattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"; + Pattern pattern = Pattern.compile(datePattern); + Matcher matcher = pattern.matcher(metadata); + String rspModifiedProvenance = metadata; + while (matcher.find()) { + String dateString = matcher.group(0); + rspModifiedProvenance = rspModifiedProvenance.replaceAll(dateString, ""); + } + return rspModifiedProvenance; + } + + private void objectCheck(DSpaceObject obj, String expectedMessage) throws Exception { + List metadata = obj.getMetadata(); + boolean contain = false; + for (MetadataValue value : metadata) { + if (!Objects.equals(value.getMetadataField().toString(), "dc_description_provenance")) { + continue; + } + if (provenanceMetadataModified(value.getValue()).contains(expectedMessage)) { + contain = true; + break; + } + } + if (!contain) { + Assert.fail("Metadata provenance do not contain expected data: " + expectedMessage); + } + } + + private Bundle createBundle(Item item, String bundleName) throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + Bundle bundle = BundleBuilder.createBundle(context, item).withName(bundleName).build(); + context.restoreAuthSystemState(); + return bundle; + } + + private Bitstream createBitstream(Item item, String bundleName) + throws SQLException, AuthorizeException, IOException { + context.turnOffAuthorisationSystem(); + Bundle bundle = createBundle(item, Objects.isNull(bundleName) ? "test" : bundleName); + Bitstream bitstream = BitstreamBuilder.createBitstream(context, bundle, + toInputStream("Test Content", defaultCharset())).build(); + context.restoreAuthSystemState(); + return bitstream; + } + + private void deleteBitstream(Bitstream bitstream) throws SQLException, IOException { + int size = bitstream.getBundles().size(); + for (int i = 0; i < size; i++) { + deleteBundle(bitstream.getBundles().get(i).getID()); + } + BitstreamBuilder.deleteBitstream(bitstream.getID()); + } + + + private void deleteBundle(UUID uuid) throws SQLException, IOException { + BundleBuilder.deleteBundle(uuid); + } + + private ClarinLicenseLabel createClarinLicenseLabel(String label, boolean extended, String title) + throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + ClarinLicenseLabel clarinLicenseLabel = ClarinLicenseLabelBuilder.createClarinLicenseLabel(context).build(); + clarinLicenseLabel.setLabel(label); + clarinLicenseLabel.setExtended(extended); + clarinLicenseLabel.setTitle(title); + clarinLicenseLabelService.update(context, clarinLicenseLabel); + context.restoreAuthSystemState(); + return clarinLicenseLabel; + } + + private ClarinLicense createClarinLicense(String name, String definition) + throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + ClarinLicense clarinLicense = ClarinLicenseBuilder.createClarinLicense(context).build(); + clarinLicense.setDefinition(definition); + clarinLicense.setName(name); + HashSet clarinLicenseLabels = new HashSet<>(); + ClarinLicenseLabel clarinLicenseLabel = createClarinLicenseLabel("lbl", false, "Test Title"); + clarinLicenseLabels.add(clarinLicenseLabel); + clarinLicense.setLicenseLabels(clarinLicenseLabels); + clarinLicenseService.update(context, clarinLicense); + context.restoreAuthSystemState(); + return clarinLicense; + } + + private void deleteClarinLicenseLable(Integer id) throws Exception { + ClarinLicenseLabelBuilder.deleteClarinLicenseLabel(id); + } + + private void deleteClarinLicense(ClarinLicense license) throws Exception { + int size = license.getLicenseLabels().size(); + for (int i = 0; i < size; i++) { + deleteClarinLicenseLable(license.getLicenseLabels().get(i).getID()); + } + ClarinLicenseBuilder.deleteClarinLicense(license.getID()); + } + + private Collection createCollection() { + context.turnOffAuthorisationSystem(); + Collection col = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + context.restoreAuthSystemState(); + return col; + } + + private void deleteCollection(UUID uuid) throws SearchServiceException, SQLException, IOException { + CollectionBuilder.deleteCollection(uuid); + } + + private ClarinLicenseResourceMapping createResourceMapping(ClarinLicense license, Bitstream bitstream) + throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + ClarinLicenseResourceMapping resourceMapping = + ClarinLicenseResourceMappingBuilder.createClarinLicenseResourceMapping(context).build(); + context.restoreAuthSystemState(); + resourceMapping.setLicense(license); + resourceMapping.setBitstream(bitstream); + return resourceMapping; + } + + private void deleteResourceMapping(Integer id) throws Exception { + ClarinLicenseResourceMappingBuilder.delete(id); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java index 423a4cbe3513..97e1491d03c9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java @@ -11,10 +11,13 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -24,6 +27,7 @@ * Utility class for performing metadata patch tests sourced from a common json file (see constructor). */ public class MetadataPatchSuite { + static String PROVENANCE = "dc.description.provenance"; private final ObjectMapper objectMapper = new ObjectMapper(); private final JsonNode suite; @@ -36,6 +40,16 @@ public MetadataPatchSuite() throws Exception { suite = objectMapper.readTree(getClass().getResourceAsStream("metadata-patch-suite.json")); } + /** + * Initializes the suite by parsing the json file of tests. + * + * @param name name of resource + * @throws Exception if there is an error reading the file. + */ + public MetadataPatchSuite(String name) throws Exception { + suite = objectMapper.readTree(getClass().getResourceAsStream(name)); + } + /** * Runs all tests in the file using the given client and url, expecting the given status. * @@ -78,13 +92,32 @@ private void checkResponse(String verb, MockMvc client, MockHttpServletRequestBu .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().is(expectedStatus)); if (expectedStatus >= 200 && expectedStatus < 300) { - String responseBody = resultActions.andReturn().getResponse().getContentAsString(); - JsonNode responseJson = objectMapper.readTree(responseBody); - String responseMetadata = responseJson.get("metadata").toString(); - if (!responseMetadata.equals(expectedMetadata)) { - Assert.fail("Expected metadata in " + verb + " response: " + expectedMetadata - + "\nGot metadata in " + verb + " response: " + responseMetadata); - } + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + JsonNode responseJson = objectMapper.readTree(responseBody); + JsonNode responseMetadataJson = responseJson.get("metadata"); + if (responseMetadataJson.get(PROVENANCE) != null) { + // In the provenance metadata, there is a timestamp indicating when they were added. + // To ensure accurate comparison, remove that date. + String rspProvenance = responseMetadataJson.get(PROVENANCE).toString(); + // Regex to match the date pattern + String datePattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"; + Pattern pattern = Pattern.compile(datePattern); + Matcher matcher = pattern.matcher(rspProvenance); + String rspModifiedProvenance = rspProvenance; + while (matcher.find()) { + String dateString = matcher.group(0); + rspModifiedProvenance = rspModifiedProvenance.replaceAll(dateString, ""); + } + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNodePrv = objectMapper.readTree(rspModifiedProvenance); + // Replace the origin metadata with a value with the timestamp removed + ((ObjectNode) responseJson.get("metadata")).put(PROVENANCE, jsonNodePrv); + } + String responseMetadata = responseJson.get("metadata").toString(); + if (!responseMetadata.equals(expectedMetadata)) { + Assert.fail("Expected metadata in " + verb + " response: " + expectedMetadata + + "\nGot metadata in " + verb + " response: " + responseMetadata); + } } } } diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/test/item-metadata-patch-suite.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/test/item-metadata-patch-suite.json new file mode 100644 index 000000000000..fdbe61278546 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/test/item-metadata-patch-suite.json @@ -0,0 +1,219 @@ +{ + "tests": [ + { + "name": "clear metadata", + "patch": [ + { "op": "replace", + "path": "/metadata", + "value": {} + } + ], + "expect": {} + }, + { + "name": "add first title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title", + "value": [ + { "value": "title 1" } + ] + } + ], + "expect": { + "dc.description.provenance" : [ + { "value" : "Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", "language" : "en", "authority" : null, "confidence" : -1, "place" : 0} + ], + "dc.title": [ + { "value": "title 1", "language": null, "authority": null, "confidence": -1, "place": 0} + ] + } + }, + { + "name": "add second title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title/-", + "value": { "value": "最後のタイトル", "language": "ja_JP" } + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1} + ], + "dc.title": [ + { "value": "title 1", "language": null, "authority": null, "confidence": -1,"place": 0 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 ,"place": 1} + ] + } + }, + { + "name": "insert zeroth title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title/0", + "value": { + "value": "title 0" + } + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":2} + ], + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 ,"place": 0 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 ,"place": 1 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 ,"place": 2 } + ] + } + }, + { + "name": "move last title up one", + "patch": [ + { + "op": "move", + "from": "/metadata/dc.title/2", + "path": "/metadata/dc.title/1" + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":2} + ], + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 ,"place": 0 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 ,"place": 1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 ,"place": 2 } + ] + } + }, + { + "name": "replace title 2 value and language in two operations", + "patch": [ + { + "op": "replace", + "path": "/metadata/dc.title/1/value", + "value": "title A" + }, + { + "op": "replace", + "path": "/metadata/dc.title/1/language", + "value": "en_US" + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":2} + ], + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 ,"place": 0 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 ,"place": 1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 ,"place": 2 } + ] + } + }, + { + "name": "copy title A to end of list", + "patch": [ + { + "op": "copy", + "from": "/metadata/dc.title/1", + "path": "/metadata/dc.title/-" + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":2} + ], + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 ,"place": 0 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 ,"place": 1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 ,"place": 2 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 ,"place": 3 } + ] + } + }, + { + "name": "remove both title A copies", + "patch": [ + { + "op": "remove", + "path": "/metadata/dc.title/1" + }, + { + "op": "remove", + "path": "/metadata/dc.title/2" + } + ], + "expect": { + "dc.description.provenance":[ + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":0}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":1}, + {"value":"Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language":"en","authority":null,"confidence":-1,"place":2}, + {"value":"Item metadata (dc.title: title A) was deleted by first (admin) last (admin) (admin@email.com) on \nNo. of bitstreams: 0", + "language":"en","authority":null,"confidence":-1,"place":3}, + {"value":"Item metadata (dc.title: title A) was deleted by first (admin) last (admin) (admin@email.com) on \nNo. of bitstreams: 0", + "language":"en","authority":null,"confidence":-1,"place":4} + ], + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 ,"place": 0 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 ,"place": 1 } + ] + } + }, + { + "name": "remove all titles", + "patch": [ + { + "op": "remove", + "path": "/metadata/dc.title" + } + ], + "expect": { + "dc.description.provenance": [ + { + "value": "Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language": "en", "authority": null, "confidence": -1, "place": 0}, + {"value": "Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language": "en", "authority": null, "confidence": -1, "place": 1}, + {"value": "Item metadata (dc.title) was added by first (admin) last (admin) (admin@email.com) on ", + "language": "en", "authority": null, "confidence": -1, "place": 2}, + {"value": "Item metadata (dc.title: title A) was deleted by first (admin) last (admin) (admin@email.com) on \nNo. of bitstreams: 0", + "language": "en", "authority": null, "confidence": -1, "place": 3}, + {"value": "Item metadata (dc.title: title A) was deleted by first (admin) last (admin) (admin@email.com) on \nNo. of bitstreams: 0", + "language": "en", "authority": null, "confidence": -1, "place": 4} + ] + } + } + ] +} diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 305d41f64ee5..f6ad5b1e5938 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -179,5 +179,7 @@ + +