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 a2d89375bf5c..4e9ad7fe9935 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 @@ -466,9 +466,6 @@ private void updateItemPolicies(Item item, BulkAccessControlInput accessControl) */ private void setItemPolicies(Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException { - - //String resPoliciesStr = extractAccessConditions(accessControl.getItem().getAccessConditions()); - accessControl .getItem() .getAccessConditions() @@ -477,10 +474,6 @@ private void setItemPolicies(Item item, BulkAccessControlInput accessControl) itemService.adjustItemPolicies(context, item, item.getOwningCollection(), false); -// if (resPoliciesStr.isEmpty()) { -// String msg = "Access condition (" + resPoliciesStr + ") was added to item"; -// addProvenanceMetadata(context, item, msg); -// } provenanceService.setItemPolicies(context, item, accessControl); } @@ -600,7 +593,7 @@ private void setBitstreamPolicies(Bitstream bitstream, Item item, BulkAccessCont itemService.adjustBitstreamPolicies(context, item, item.getOwningCollection(), bitstream); mediaFilterService.updatePoliciesOfDerivativeBitstreams(context, item, bitstream); - provenanceService.setBitstreaPolicies(context, bitstream, item, accessControl); + provenanceService.setBitstreamPolicies(context, bitstream, item, accessControl); } /** diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProvider.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProvider.java new file mode 100644 index 000000000000..56c827ce82b7 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProvider.java @@ -0,0 +1,28 @@ +/** + * 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 org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.content.Bitstream; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; + +import java.sql.SQLException; +import java.util.List; + +public interface ProvenanceMessageProvider { + public String getMessage(Context context, String templateKey, Object... args) throws SQLException, AuthorizeException; + public String getMessage(Context context, String templateKey, Item item, Object... args) throws SQLException, AuthorizeException; + public String getMessage(String templateKey, Object... args); + public String addCollectionsToMessage(Item item) throws SQLException, AuthorizeException; + public String getBitstreamMessage(Bitstream bitstream); + public String getResourcePoliciesMessage(List resPolicies); + public String getMetadata(String oldMtdKey, String oldMtdValue); + public String getMetadataField(MetadataField metadataField); +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProviderImpl.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProviderImpl.java new file mode 100644 index 000000000000..cdef2dbddfe9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceMessageProviderImpl.java @@ -0,0 +1,122 @@ +/** + * 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 com.fasterxml.jackson.databind.ObjectMapper; +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; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ProvenanceMessageProviderImpl implements ProvenanceMessageProvider { + private Map messageTemplates; + + @Autowired + private InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); + + @Autowired + public ProvenanceMessageProviderImpl() { + loadMessageTemplates(); + } + + private void loadMessageTemplates() { + ObjectMapper mapper = new ObjectMapper(); + try { + messageTemplates = mapper.readValue(Files.readAllBytes(Paths.get("C:\\workspace\\DSpace\\dspace-api\\src\\main\\java\\org\\dspace\\core\\provenance_messages.json")), Map.class); + } catch (IOException e) { + throw new RuntimeException("Failed to load message templates", e); + } + } + + @Override + public String getMessage(Context context, String templateKey, Item item, Object... args) throws SQLException, AuthorizeException { + String msg = getMessage(context, templateKey, args); + msg = msg + "\n" + installItemService.getBitstreamProvenanceMessage(context, item); + return msg; + } + + @Override + public String getMessage(String templateKey, Object... args) { + String template = messageTemplates.get(templateKey); + if (template == null) { + throw new IllegalArgumentException("No message template found for key: " + templateKey); + } + return String.format(template, args); + } + + @Override + public String getMessage(Context context, String templateKey, Object... args) { + EPerson currentUser = context.getCurrentUser(); + String timestamp = DCDate.getCurrent().toString(); + String details = getMessage(templateKey, args); + return String.format("%s by %s (%s) on %s", + details, + currentUser.getFullName(), + currentUser.getEmail(), + timestamp); + } + + @Override + public String addCollectionsToMessage(Item item) throws SQLException, AuthorizeException { + 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; + } + + @Override + public String getBitstreamMessage(Bitstream bitstream) { + // values of deleted bitstream + String msg = bitstream.getName() + ": " + + bitstream.getSizeBytes() + " bytes, checksum: " + + bitstream.getChecksum() + " (" + + bitstream.getChecksumAlgorithm() + ")\n"; + return msg; + } + + @Override + public String getResourcePoliciesMessage(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(";")); + } + + @Override + public String getMetadata(String oldMtdKey, String oldMtdValue) { + return oldMtdKey + ": " + oldMtdValue; + } + + @Override + public String getMetadataField(MetadataField metadataField) { + return metadataField.toString() + .replace('_', '.'); + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java index 5f7ce1d59b09..6f8fa5592678 100644 --- a/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceService.java @@ -1,17 +1,43 @@ +/** + * 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 org.dspace.app.bulkaccesscontrol.model.BulkAccessControlInput; import org.dspace.authorize.AuthorizeException; 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; import java.sql.SQLException; +import java.util.List; public interface ProvenanceService { - public void setBitstreaPolicies(Context context, Bitstream bitstream, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException; + public void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException; public void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException; public String removedReadPolicies(Context context, DSpaceObject dso, String type) throws SQLException, AuthorizeException; + public void uploadBitstream(Context context, Bundle bundle) throws SQLException, AuthorizeException; + public void editLicense(Context context, Item item, boolean newLicense) throws SQLException, AuthorizeException; + + public void moveItem(Context context, Item item, Collection collection) throws SQLException, AuthorizeException; + public void mappedItem(Context context, Item item, Collection collection) throws SQLException, AuthorizeException; + public void deletedItemFromMapped(Context context, Item item, Collection collection) throws SQLException, AuthorizeException; + public void deleteBitstream(Context context,Bitstream bitstream) throws SQLException, AuthorizeException; + public void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField) throws SQLException, AuthorizeException; + public void removeMetadata(Context context, DSpaceObject dso, MetadataField metadataField) throws SQLException, AuthorizeException; + public void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt) throws SQLException, AuthorizeException; + public void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal) throws SQLException, AuthorizeException; + public void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal) throws SQLException, AuthorizeException; + public void makeDiscoverable(Context context, Item item, boolean discoverable) throws SQLException, AuthorizeException; } diff --git a/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java index c0672edca657..6e7aba1ab5a3 100644 --- a/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/ProvenanceServiceImpl.java @@ -1,38 +1,64 @@ +/** + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +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.authorize.service.ResourcePolicyService; import org.dspace.content.Bitstream; -import org.dspace.content.DCDate; +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.factory.ClarinServiceFactory; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.InstallItemService; +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.eperson.EPerson; +import org.dspace.content.service.clarin.ClarinLicenseResourceMappingService; import org.springframework.beans.factory.annotation.Autowired; -import java.io.File; -import java.io.IOException; import java.sql.SQLException; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; public class ProvenanceServiceImpl implements ProvenanceService { + private static final Logger log = LogManager.getLogger(); + @Autowired private ItemService itemService; @Autowired private ResourcePolicyService resourcePolicyService; + @Autowired + private ClarinItemService clarinItemService; + @Autowired + private ClarinLicenseResourceMappingService clarinResourceMappingService; + @Autowired + private BitstreamService bitstreamService; + + private ProvenanceMessageProvider messageProvider = new ProvenanceMessageProviderImpl(); + - protected ProvenanceServiceImpl() { + + private void addProvenanceMetadata(Context context, Item item, String msg) + throws SQLException, AuthorizeException { + itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), + "description", "provenance", "en", msg.toString()); + itemService.update(context, item); // Update item in DB + //context.commit(); } private String extractAccessConditions(List accessConditions) { @@ -41,94 +67,220 @@ private String extractAccessConditions(List accessConditions) { .collect(Collectors.joining(";")); } + private Item getItem(Context context, Bitstream bitstream) throws SQLException { + List items = clarinItemService.findByBitstreamUUID(context, bitstream.getID()); + if (items.isEmpty()) { + log.warn("Bitstream (" + bitstream.getID() + ") is not assigned to any item."); + return null; + } + return items.get(0); + } + + @Override + public void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException { + String resPoliciesStr = extractAccessConditions(accessControl.getItem().getAccessConditions()); + if (!resPoliciesStr.isEmpty()) { + String msg = messageProvider.getMessage(context,"accessCondition", resPoliciesStr, + "item", item.getID()); + addProvenanceMetadata(context, item, msg); + } + } + @Override public String removedReadPolicies(Context context, DSpaceObject dso, String type) throws SQLException, AuthorizeException { List resPolicies = resourcePolicyService.find(context, dso, type); if (resPolicies.isEmpty()) { return null; } - String resPoliciesStr = extractResourcePolicies(resPolicies); + String resPoliciesStr = messageProvider.getResourcePoliciesMessage(resPolicies); if (dso.getType() == Constants.ITEM) { - addProvenanceForItem(context, (Item) dso, resPoliciesStr); + Item item = (Item) dso; + String msg = messageProvider.getMessage(context,"resourcePoliciesRemoved", + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "item", item.getID()); + addProvenanceMetadata(context, item, msg); } else if (dso.getType() == Constants.BITSTREAM) { - addProvenanceForBitstream(context, (Bitstream) dso, resPoliciesStr); + Bitstream bitstream = (Bitstream) dso; + Item item = getItem(context, bitstream); + if (!Objects.isNull(item)) { + String msg = messageProvider.getMessage(context,"resourcePoliciesRemoved", + resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr, "bitstream", bitstream.getID()); + addProvenanceMetadata(context, item, msg); + } } return resPoliciesStr; } - private String extractResourcePolicies(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(";")); + @Override + public void setBitstreamPolicies(Context context, Bitstream bitstream, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException { + String accConditionsStr = extractAccessConditions(accessControl.getBitstream().getAccessConditions()); + if (!accConditionsStr.isEmpty()) { + String msg = messageProvider.getMessage(context,"accessCondition", accConditionsStr, + "bitstream", bitstream.getID()); + addProvenanceMetadata(context, item, msg); + } } @Override - public void setItemPolicies(Context context, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException { - String resPoliciesStr = extractAccessConditions(accessControl.getItem().getAccessConditions()); - if (!resPoliciesStr.isEmpty()) { - String msg = "Access condition (" + resPoliciesStr + ") was added to item"; + public void editLicense(Context context, Item item, boolean newLicense) throws SQLException, AuthorizeException { + String oldLicense = null; + List bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME); + for (Bundle clarinBundle : bundles) { + List bitstreamList = clarinBundle.getBitstreams(); + for (Bitstream bundleBitstream : bitstreamList) { + if (Objects.isNull(oldLicense)) { + List mappings = + this.clarinResourceMappingService.findByBitstreamUUID( + context, bundleBitstream.getID()); + if (!mappings.isEmpty()) { + oldLicense = mappings.get(0).getLicense().getName(); + } + } + } + } + + String msg = messageProvider.getMessage(context, "editLicense", item, + Objects.isNull(oldLicense) ? "empty" : oldLicense, + !newLicense ? "removed" : Objects.isNull(oldLicense) ? "added" : "updated"); + addProvenanceMetadata(context, item, msg); + } + + + @Override + public void moveItem(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { + String msg = messageProvider.getMessage(context, "moveItem", 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(); + } + + @Override + public void mappedItem(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { + String msg = messageProvider.getMessage(context, "mappedItem", + item.getID(), collection.getID()); + addProvenanceMetadata(context, item, msg); + } + + @Override + public void deletedItemFromMapped(Context context, Item item, Collection collection) throws SQLException, AuthorizeException { + String msg = messageProvider.getMessage(context, "deletedItemFromMapped", + item.getID(), collection.getID()); + addProvenanceMetadata(context, item, msg); + } + + @Override + public void deleteBitstream(Context context,Bitstream bitstream) throws SQLException, AuthorizeException { + Item item = getItem(context, bitstream); + if (!Objects.isNull(item)) { + String msg = messageProvider.getMessage(context, "editBitstream", item, + item.getID(), messageProvider.getBitstreamMessage(bitstream)); addProvenanceMetadata(context, item, msg); } } @Override - public void setBitstreaPolicies(Context context, Bitstream bitstream, Item item, BulkAccessControlInput accessControl) throws SQLException, AuthorizeException { - String accConditionsStr = extractAccessConditions(accessControl.getBitstream().getAccessConditions()); - if (!accConditionsStr.isEmpty()) { + public void addMetadata(Context context, DSpaceObject dso, MetadataField metadataField) throws SQLException, AuthorizeException { + if (Constants.ITEM == dso.getType()) { // Add suitable provenance - String msg = "Access condition (" + accConditionsStr + ") was added to bitstream (" + - bitstream.getID() + ") of item"; + String msg = messageProvider.getMessage(context, "itemMetadata", + messageProvider.getMetadataField(metadataField), "added"); + addProvenanceMetadata(context, (Item) dso, msg); + } + + if (dso.getType() == Constants.BITSTREAM) { + // Add suitable provenance + Bitstream bitstream = (Bitstream) dso; + Item item = getItem(context, bitstream); + if (!Objects.isNull(item)) { + String msg = messageProvider.getMessage(context, "bitstreamMetadata", item, + messageProvider.getMetadataField(metadataField), "added by", + messageProvider.getBitstreamMessage(bitstream)); + addProvenanceMetadata(context, item, msg); + } + } + } + + @Override + public void removeMetadata(Context context, DSpaceObject dso, MetadataField metadataField) throws SQLException, AuthorizeException { + if (dso.getType() != Constants.BITSTREAM) { + return; + } + String oldMtdKey = null; + String oldMtdValue = null; + List mtd = bitstreamService.getMetadata((Bitstream) dso, metadataField.getMetadataSchema().getName(), + metadataField.getElement(), metadataField.getQualifier(), Item.ANY); + if (!CollectionUtils.isEmpty(mtd)) { + oldMtdKey = mtd.get(0).getMetadataField().getElement(); + oldMtdValue = mtd.get(0).getValue(); + } + // Add suitable provenance + Bitstream bitstream = (Bitstream) dso; + Item item = getItem(context, bitstream); + if (!Objects.isNull(item)) { + String msg = messageProvider.getMessage(context, "bitstreamMetadata", item, + messageProvider.getMetadata(oldMtdKey, oldMtdValue), + "deleted from", messageProvider.getBitstreamMessage(bitstream)); addProvenanceMetadata(context, item, msg); } } - /** - * Adds provenance metadata to an item, documenting changes made to its resource policies - * and bitstream. This method records the user who performed the action, the action taken, - * and the timestamp of the action. It also appends a bitstream provenance message generated - * by the InstallItemService. - * - * @param context the current DSpace context, which provides details about the current user - * and authorization information. - * @param item the DSpace item to which the provenance metadata should be added. - * @param msg a custom message describing the action taken on the resource policies. - * @throws SQLException if there is a database access error while updating the item. - * @throws AuthorizeException if the current user is not authorized to add metadata to the item. - */ - private void addProvenanceMetadata(Context context, Item item, String msg) - throws SQLException, AuthorizeException { - InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); - String timestamp = DCDate.getCurrent().toString(); - EPerson e = context.getCurrentUser(); - StringBuilder prov = new StringBuilder(); - prov.append(msg).append(" by ").append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ") - .append(timestamp).append("\n"); - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - //Update item in DB - itemService.update(context, item); + @Override + public void removeMetadataAtIndex(Context context, DSpaceObject dso, List metadataValues, + int indexInt) throws SQLException, AuthorizeException { + if (dso.getType() != Constants.ITEM) { + return; + } + // Remember removed mtd + String oldMtdKey = messageProvider.getMetadataField(metadataValues.get(indexInt).getMetadataField()); + String oldMtdValue = metadataValues.get(indexInt).getValue(); + // Add suitable provenance + String msg = messageProvider.getMessage(context, "itemMetadata", + (Item) dso, messageProvider.getMetadata(oldMtdKey, oldMtdValue), "deleted"); + addProvenanceMetadata(context, (Item) dso, msg); } - private void addProvenanceForItem(Context context, Item item, String resPoliciesStr) throws SQLException, AuthorizeException { - String msg = "Resource policies (" + (resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr) - + ") of item (" + item.getID() + ") were removed"; - addProvenanceMetadata(context, item, msg); + + @Override + public void replaceMetadata(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal) throws SQLException, AuthorizeException { + if (dso.getType() != Constants.ITEM) { + return; + } + String msg = messageProvider.getMessage(context, "itemMetadata",(Item) dso, + messageProvider.getMetadata(messageProvider.getMetadataField(metadataField), oldMtdVal), + "updated"); + addProvenanceMetadata(context, (Item) dso, msg); } - private void addProvenanceForBitstream(Context context, Bitstream bitstream, String resPoliciesStr) throws SQLException, AuthorizeException { - ClarinItemService clarinItemService = ClarinServiceFactory.getInstance().getClarinItemService(); - List items = clarinItemService.findByBitstreamUUID(context, bitstream.getID()); - if (!items.isEmpty()) { - Item item = items.get(0); - String msg = "Resource policies (" + (resPoliciesStr.isEmpty() ? "empty" : resPoliciesStr) - + ") of bitstream (" + bitstream.getID() + ") were removed"; - addProvenanceMetadata(context, item, msg); + @Override + public void replaceMetadataSingle(Context context, DSpaceObject dso, MetadataField metadataField, String oldMtdVal) throws SQLException, AuthorizeException { + if (dso.getType() != Constants.BITSTREAM) { + return; } + + // Add suitable provenance + Bitstream bitstream = (Bitstream) dso; + Item item = getItem(context, bitstream); + if (!Objects.isNull(item)) { + String msg = messageProvider.getMessage(context, "itemReplaceSingleMetadata", item, + messageProvider.getBitstreamMessage(bitstream), + messageProvider.getMetadata(messageProvider.getMetadataField(metadataField), oldMtdVal)); + addProvenanceMetadata(context, item, msg);; + } + } + + @Override + public void makeDiscoverable(Context context, Item item, boolean discoverable) throws SQLException, AuthorizeException { + // Add suitable provenance + String msg = messageProvider.getMessage(context, "discoverable", + item, discoverable ? "" : "non-") + messageProvider.addCollectionsToMessage(item); + addProvenanceMetadata(context, item, msg); + } + + @Override + public void uploadBitstream(Context context, Bundle bundle) throws SQLException, AuthorizeException { + Item item = bundle.getItems().get(0); + String msg = messageProvider.getMessage(context, "bundleAdded", item, bundle.getID()); + addProvenanceMetadata(context,item, msg); + itemService.update(context, item); } } diff --git a/dspace-api/src/main/java/org/dspace/core/provenance_messages.json b/dspace-api/src/main/java/org/dspace/core/provenance_messages.json new file mode 100644 index 000000000000..155e1d3d7a00 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/provenance_messages.json @@ -0,0 +1,15 @@ +{ + "accessCondition": "Access condition (%s) was added to %s (%s)", + "resourcePoliciesRemoved": "Resource policies (%s) of (%s) (%s) were removed", + "bundleAdded": "Item was added bitstream to bundle (%s)", + "editLicense": "License (%s) was %s", + "moveItem": "Item was moved from collection (%s) to different collection", + "mappedItem": "Item (%s) was mapped to collection (%s)", + "deletedItemFromMapped": "Item (%s) was deleted from mapped collection (%s)", + "editBitstream": "Item (%s) was deleted bitstream (%s)", + "itemMetadata": "Item metadata (%s) was %s", + "bitstreamMetadata": "Item metadata (%s) was %s bitstream (%s)", + "itemMetadata": "Item metadata (%s) was %s", + "itemReplaceSingleMetadata": "Item bitstream (%s) metadata (%s) was updated", + "discoverable": "Item was made %sdiscoverable" +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java index 9bf42de43f10..749cd788a4c3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BundleUploadBitstreamController.java @@ -27,14 +27,9 @@ import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bundle; -import org.dspace.content.DCDate; -import org.dspace.content.Item; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.service.BundleService; -import org.dspace.content.service.InstallItemService; -import org.dspace.content.service.ItemService; import org.dspace.core.Context; -import org.dspace.eperson.EPerson; +import org.dspace.core.ProvenanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ControllerUtils; import org.springframework.data.rest.webmvc.ResourceNotFoundException; @@ -78,18 +73,15 @@ public class BundleUploadBitstreamController { @Autowired private BundleService bundleService; - @Autowired - private ItemService itemService; - - @Autowired - private InstallItemService installItemService; - @Autowired private BundleRestRepository bundleRestRepository; @Autowired private ConverterService converter; + @Autowired + private ProvenanceService provenanceService; + /** * Method to upload a Bitstream to a Bundle with the given UUID in the URL. This will create a Bitstream with the * file provided in the request and attach this to the Item that matches the UUID in the URL. @@ -123,26 +115,11 @@ public ResponseEntity> uploadBitstream( e); throw new UnprocessableEntityException("The InputStream from the file couldn't be read", e); } - - // We do this before calling `updateBitstream` because that function calls `context.commit` - // Add suitable provenance - EPerson e = context.getCurrentUser(); - String timestamp = DCDate.getCurrent().toString(); - Item item = bundle.getItems().get(0); - - StringBuilder prov = new StringBuilder(); - prov.append("Item was added bitstream to bundle (").append(bundle.getID()) - .append(") by ").append(e.getFullName()).append(" (").append(e.getEmail()) - .append(") on ").append(timestamp).append("\n"); try { - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - //Update item in DB - itemService.update(context, item); + provenanceService.uploadBitstream(context, bundle); } catch (SQLException ex) { throw new RuntimeException("SQLException in BundleUploadBitstreamConverter.uploadBitstream when " + - "adding new provenance metadata.", ex); + "adding new provenance metadata.", ex); } catch (AuthorizeException ex) { throw new RuntimeException("AuthorizeException in BundleUploadBitstreamConverter.uploadBitstream " + "when adding new provenance metadata.", ex); 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 846264c29bd3..a4dafe630dfc 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 @@ -42,6 +42,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.dspace.eperson.EPerson; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +105,7 @@ public class ItemAddBundleController { ClarinLicenseResourceMappingService clarinLicenseResourceMappingService; @Autowired - InstallItemService installItemService; + ProvenanceService provenanceService; /** * Method to add a Bundle to an Item with the given UUID in the URL. This will create a Bundle with the @@ -173,19 +174,11 @@ public ItemRest updateLicenseForBundle(@PathVariable UUID uuid, log.warn("Cannot find clarin license with id: " + licenseId + ". The old license will be detached, " + "but the new one will not be attached."); } + provenanceService.editLicense(context, item, Objects.isNull(clarinLicense)); List bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME); - String oldLicense = null; for (Bundle clarinBundle : bundles) { List bitstreamList = clarinBundle.getBitstreams(); for (Bitstream bundleBitstream : bitstreamList) { - if (Objects.isNull(oldLicense)) { - List mappings = - this.clarinLicenseResourceMappingService.findByBitstreamUUID( - context, bundleBitstream.getID()); - if (!mappings.isEmpty()) { - oldLicense = mappings.get(0).getLicense().getName(); - } - } // in case bitstream ID exists in license table for some reason .. just remove it this.clarinLicenseResourceMappingService.detachLicenses(context, bundleBitstream); if (Objects.nonNull(clarinLicense)) { @@ -198,22 +191,6 @@ public ItemRest updateLicenseForBundle(@PathVariable UUID uuid, if (Objects.nonNull(clarinLicense)) { clarinLicenseService.addLicenseMetadataToItem(context, clarinLicense, item); } - - // Add suitable provenance - EPerson e = context.getCurrentUser(); - String timestamp = DCDate.getCurrent().toString(); - StringBuilder prov = new StringBuilder(); - - prov.append("License (").append(Objects.isNull(oldLicense) ? "empty" : oldLicense).append(") was ") - .append(Objects.isNull(clarinLicense) ? "removed" : Objects.isNull(oldLicense) ? "added" : "updated") - .append(" by ").append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ") - .append(timestamp).append("\n"); - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - - // Update item in DB itemService.update(context, item); context.commit(); 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 76831cb67409..c9cbb4ea9a10 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 @@ -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.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ResourceNotFoundException; @@ -56,8 +57,6 @@ public class ItemOwningCollectionUpdateRestController { @Autowired ItemService itemService; - @Autowired - InstallItemService installItemService; @Autowired CollectionService collectionService; @@ -71,6 +70,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. @@ -131,26 +133,7 @@ private Collection moveItem(final Context context, final Item item, final Collec final boolean inheritPolicies) throws SQLException, IOException, AuthorizeException { itemService.move(context, item, currentCollection, targetCollection, inheritPolicies); - - // Add suitable provenance - String timestamp = DCDate.getCurrent().toString(); - EPerson e = context.getCurrentUser(); - StringBuilder prov = new StringBuilder(); - - prov.append("Item was moved from collection (") - .append(currentCollection.getID()).append(") to different collection by ").append(e.getFullName()) - .append(" (").append(e.getEmail()).append(") on ").append(timestamp).append("\n"); - - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - // Update item in DB - // Because a user can move an item without authorization turn off authorization - context.turnOffAuthorisationSystem(); - itemService.update(context, item); - context.restoreAuthSystemState(); - + provenanceService.moveItem(context, item, currentCollection); // Necessary because Controller does not pass through general RestResourceController, and as such does not do // its commit in DSpaceRestRepository.createAndReturn() or similar context.commit(); 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 8cacad9927cd..2ba35bbac229 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 @@ -33,6 +33,7 @@ import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -55,15 +56,15 @@ public class MappedCollectionRestController { @Autowired private ItemService itemService; - @Autowired - private InstallItemService installItemService; - @Autowired private CollectionService collectionService; @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 @@ -113,8 +114,7 @@ public void createCollectionToItemRelation(@PathVariable UUID uuid, collectionService.update(context, collectionToMapTo); // Add suitable provenance - String msg = "was mapped to collection"; - addApprovedProvenance(context, item, msg, collectionToMapTo); + provenanceService.mappedItem(context, item, collectionToMapTo); itemService.update(context, item); } else { @@ -164,8 +164,7 @@ public void deleteCollectionToItemRelation(@PathVariable UUID uuid, @PathVariabl collectionService.update(context, collection); // Add suitable provenance - String msg = "was deleted from mapped collection"; - addApprovedProvenance(context, item, msg, collection); + provenanceService.deletedItemFromMapped(context,item, collection); itemService.update(context, item); context.commit(); @@ -175,17 +174,6 @@ public void deleteCollectionToItemRelation(@PathVariable UUID uuid, @PathVariabl } } - public void addApprovedProvenance(Context context, Item item, String msg, Collection col) throws SQLException { - EPerson e = context.getCurrentUser(); - String timestamp = DCDate.getCurrent().toString(); - StringBuilder prov = new StringBuilder(); - prov.append("Item ").append(msg).append(" (").append(col.getID()).append(") by ") - .append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ").append(timestamp); - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), "description", - "provenance", "en", prov.toString()); - } - private void checkIfItemIsTemplate(Item item) { if (item.getTemplateItemOf() != null) { throw new MethodNotAllowedException("Given item is a template item."); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java index 14d8c623304d..3de199de7319 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java @@ -52,15 +52,6 @@ public class BitstreamRemoveOperation extends PatchOperation { @Autowired BitstreamService bitstreamService; - @Autowired - ItemService itemService; - - @Autowired - ClarinItemService clarinItemService; - - @Autowired - InstallItemService installItemService; - @Autowired AuthorizeService authorizeService; public static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; @@ -73,49 +64,10 @@ public Bitstream perform(Context context, Bitstream resource, Operation operatio throw new RESTBitstreamNotFoundException(bitstreamIDtoDelete); } authorizeBitstreamRemoveAction(context, bitstreamToDelete, Constants.DELETE); - try { - List items = clarinItemService.findByBitstreamUUID(context, UUID.fromString(bitstreamIDtoDelete)); - // The bitstream is assigned only into one Item. - Item item = null; - if (!CollectionUtils.isEmpty(items)) { - item = items.get(0); - } - - // values of deleted bitstream - StringBuilder bitstreamMsg = new StringBuilder(); - bitstreamMsg.append(bitstreamToDelete.getName()).append(": ") - .append(bitstreamToDelete.getSizeBytes()).append(" bytes, checksum: ") - .append(bitstreamToDelete.getChecksum()).append(" (") - .append(bitstreamToDelete.getChecksumAlgorithm()).append(")\n"); - - //delete bitstream + provenanceService.deleteBitstream(context, bitstreamToDelete); bitstreamService.delete(context, bitstreamToDelete); - - if (item == null) { - return null; - } - - // Add suitable provenance - String timestamp = DCDate.getCurrent().toString(); - EPerson e = context.getCurrentUser(); - - // Build some provenance data while we're at it. - StringBuilder prov = new StringBuilder(); - prov.append("Item was deleted bitstream (").append(bitstreamMsg).append(") by ") - .append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ") - .append(timestamp).append("\n"); - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - //Update item in DB - itemService.update(context, item); - } catch (AuthorizeException e) { - throw new DSpaceBadRequestException( - "AuthorizeException in BitstreamRemoveOperation.perform while adding provenance metadata " + - "about the deleted item's bitstream.", e); - } catch (IOException e) { + } catch (AuthorizeException | IOException e) { throw new RuntimeException(e.getMessage(), e); } return null; 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 49af741bf675..67fd804b1622 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 @@ -49,15 +49,6 @@ public class DSpaceObjectMetadataAddOperation extends Pa @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; - @Autowired - InstallItemService installItemService; - - @Autowired - ItemService itemService; - - @Autowired - ClarinItemService clarinItemService; - @Override public R perform(Context context, R resource, Operation operation) throws SQLException { DSpaceObjectService dsoService = ContentServiceFactory.getInstance().getDSpaceObjectService(resource); @@ -90,40 +81,10 @@ 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); - - if (dso.getType() != Constants.ITEM && dso.getType() != Constants.BITSTREAM) { - return; - } - - if (dso.getType() == Constants.ITEM) { - // Add suitable provenance - Item item = (Item) dso; - String msg = "metadata (" + metadataField.toString() - .replace('_', '.') + ") was added"; - addProvenanceMetadata(context, item, msg); - } - - if (dso.getType() == Constants.BITSTREAM) { - // Add suitable provenance - Bitstream bitstream = (Bitstream) dso; - List items = clarinItemService.findByBitstreamUUID(context, dso.getID()); - // The bitstream is assigned only into one Item. - Item item = null; - if (CollectionUtils.isEmpty(items)) { - log.warn("Bitstream (" + dso.getID() + ") is not assigned to any item."); - return; - } - item = items.get(0); - String msg = "metadata (" + - metadataField.toString().replace('_', '.') + ") was added" - + " to bitstream (" + bitstream.getName() + ": " + bitstream.getSizeBytes() - + " bytes, checksum: " + bitstream.getChecksum() + " (" + - bitstream.getChecksumAlgorithm() + ")"; - addProvenanceMetadata(context, item, msg); - } + provenanceService.addMetadata(context, dso, metadataField); } catch (SQLException e) { - throw new DSpaceBadRequestException("SQLException in DspaceObjectMetadataAddOperation.add trying to add " + - "metadata to dso.", e); + throw new DSpaceBadRequestException("SQLException in DspaceObjectMetadataAddOperation.add trying to add " + + "metadata to dso.", e); } catch (AuthorizeException e) { throw new DSpaceBadRequestException( "AuthorizeException in DspaceObjectMetadataAddOperation.add " + 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 119325bd4997..c024edf561f2 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 @@ -54,15 +54,6 @@ public class DSpaceObjectMetadataRemoveOperation extends @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; - @Autowired - InstallItemService installItemService; - - @Autowired - private ClarinItemService clarinItemService; - - @Autowired - private ItemService itemService; - @Override public R perform(Context context, R resource, Operation operation) throws SQLException { DSpaceObjectService dsoService = ContentServiceFactory.getInstance().getDSpaceObjectService(resource); @@ -87,36 +78,10 @@ private void remove(Context context, DSpaceObject dso, DSpaceObjectService dsoSe metadataPatchUtils.checkMetadataFieldNotNull(metadataField); try { if (index == null) { - String oldMtdKey = null; - String oldMtdValue = null; - if (dso.getType() == Constants.BITSTREAM) { - List mtd = dsoService.getMetadata(dso, metadataField.getMetadataSchema().getName(), - metadataField.getElement(), metadataField.getQualifier(), Item.ANY); - if (!CollectionUtils.isEmpty(mtd)) { - oldMtdKey = mtd.get(0).getMetadataField().getElement(); - oldMtdValue = mtd.get(0).getValue(); - } - } + provenanceService.removeMetadata(context, dso, metadataField); // remove all metadata of this type dsoService.clearMetadata(context, dso, metadataField.getMetadataSchema().getName(), metadataField.getElement(), metadataField.getQualifier(), Item.ANY); - if (dso.getType() != Constants.BITSTREAM) { - return; - } - // Add suitable provenance - Bitstream bitstream = (Bitstream) dso; - List items = clarinItemService.findByBitstreamUUID(context, dso.getID()); - // The bitstream is assigned only into one Item. - Item item = null; - if (CollectionUtils.isEmpty(items)) { - log.warn("Bitstream (" + dso.getID() + ") is not assigned to any item."); - return; - } - item = items.get(0); - String msg = " metadata (" + oldMtdKey + ": " + oldMtdValue + ") was deleted from bitstream (" + - bitstream.getName() + ": " + bitstream.getSizeBytes() + " bytes, checksum: " + - bitstream.getChecksum() + " (" + bitstream.getChecksumAlgorithm() + "))"; - addProvenanceMetadata(context, item, msg); } else { // remove metadata at index List metadataValues = dsoService.getMetadata(dso, @@ -125,26 +90,10 @@ private void remove(Context context, DSpaceObject dso, DSpaceObjectService dsoSe int indexInt = Integer.parseInt(index); if (indexInt >= 0 && metadataValues.size() > indexInt && metadataValues.get(indexInt) != null) { - // Remember removed mtd - String oldMtdKey = null; - String oldMtdValue = null; - if (dso.getType() == Constants.ITEM) { - oldMtdKey = metadataValues.get(indexInt).getMetadataField().toString() - .replace('_', '.'); - oldMtdValue = metadataValues.get(indexInt).getValue(); - } + provenanceService.removeMetadataAtIndex(context, dso, metadataValues, indexInt); // remove that metadata dsoService.removeMetadataValues(context, dso, Arrays.asList(metadataValues.get(indexInt))); - - if (dso.getType() != Constants.ITEM) { - return; - } - - // Add suitable provenance - Item item = (Item) dso; - String msg = "metadata (" + oldMtdKey + ": " + oldMtdValue + ") was deleted"; - addProvenanceMetadata(context, item, msg); } else { throw new UnprocessableEntityException("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/DSpaceObjectMetadataReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java index 141300c2175c..490358eddb65 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 @@ -49,20 +49,9 @@ */ @Component public class DSpaceObjectMetadataReplaceOperation extends PatchOperation { - private static final Logger log = LogManager.getLogger(); - @Autowired DSpaceObjectMetadataPatchUtils metadataPatchUtils; - @Autowired - InstallItemService installItemService; - - @Autowired - ItemService itemService; - - @Autowired - ClarinItemService clarinItemService; - @Override public R perform(Context context, R resource, Operation operation) throws SQLException { DSpaceObjectService dsoService = ContentServiceFactory.getInstance().getDSpaceObjectService(resource); @@ -185,16 +174,8 @@ private void replaceSingleMetadataValue(Context context, DSpaceObject dso, DSpac existingMdv.setLanguage(metadataValue.getLanguage()); existingMdv.setValue(metadataValue.getValue()); dsoService.setMetadataModified(dso); + provenanceService.replaceMetadata(context, dso, metadataField, oldMtdVal); - if (dso.getType() != Constants.ITEM) { - return; - } - - // Add suitable provenance - Item item = (Item) dso; - String msg = "metadata (" + metadataField.toString() - .replace('_', '.') + ": " + oldMtdVal + ")" + " was updated"; - addProvenanceMetadata(context, item, msg); } else { throw new UnprocessableEntityException("There is no metadata of this type at that index"); } @@ -246,30 +227,7 @@ private void replaceSinglePropertyOfMdValue(Context context, DSpaceObject dso, D existingMdv.setValue(valueMdProperty); } dsoService.setMetadataModified(dso); - - if (dso.getType() != Constants.BITSTREAM) { - return; - } - - // Add suitable provenance - Bitstream bitstream = (Bitstream) dso; - String timestamp = DCDate.getCurrent().toString(); - EPerson e = context.getCurrentUser(); - List items = clarinItemService.findByBitstreamUUID(context, dso.getID()); - // The bitstream is assigned only into one Item. - Item item = null; - if (CollectionUtils.isEmpty(items)) { - log.warn("Bitstream (" + dso.getID() + ") is not assigned to any item."); - return; - } - item = items.get(0); - String msg = "bitstream (" + bitstream.getName() + ": " + - bitstream.getSizeBytes() + " bytes, checksum: " + - bitstream.getChecksum() + " (" + - bitstream.getChecksumAlgorithm() + ")" + ") metadata (" + - metadataField.toString().replace('_', '.') + ": " + oldMtdVal + ") was updated " + - "by " + e.getFullName() + "(" + e.getEmail() + ") on " + timestamp; - addProvenanceMetadata(context, item, msg); + 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 66b5226ad404..e664005e4cf6 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 @@ -42,12 +42,6 @@ public class ItemDiscoverableReplaceOperation extends PatchOperation { */ private static final String OPERATION_PATH_DISCOVERABLE = "/discoverable"; - @Autowired - ItemService itemService; - - @Autowired - InstallItemService installItemService; - @Override public R perform(Context context, R object, Operation operation) { checkOperationValue(operation.getValue()); @@ -58,25 +52,9 @@ public R perform(Context context, R object, Operation operation) { throw new UnprocessableEntityException("A template item cannot be discoverable."); } item.setDiscoverable(discoverable); - - // Add suitable provenance - EPerson e = context.getCurrentUser(); - String timestamp = DCDate.getCurrent().toString(); - // Build some provenance data. - StringBuilder prov = new StringBuilder(); - prov.append("Item was made ").append( discoverable ? "" : "non-").append("discoverable by ") - .append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ").append(timestamp) - .append("\n").append("Item was in collections:\n"); - List colls = item.getCollections(); - for (Collection coll : colls) { - prov.append(coll.getName()).append(" (ID: ").append(coll.getID()).append(")\n"); - } - try { - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), "description", - "provenance", "en", prov.toString()); - itemService.update(context, item); + provenanceService.makeDiscoverable(context, item, discoverable); + //tu } catch (SQLException ex) { throw new RuntimeException("SQLException occurred when item making " + (discoverable ? "" : "non-") + "discoverable.", ex); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java index 02388421bdda..a737eea74c58 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java @@ -19,6 +19,7 @@ import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.core.ProvenanceService; import org.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; @@ -40,6 +41,9 @@ public abstract class PatchOperation { @Autowired ItemService itemService; + @Autowired + ProvenanceService provenanceService; + /** * Updates the rest model by applying the patch operation. * @@ -88,34 +92,6 @@ Boolean getBooleanOperationValue(Object value) { return bool; } - /** - * Adds provenance metadata to an item, documenting an action taken on the item. - * This method logs the user who performed the action, the action description, - * and the timestamp when the action occurred. It also appends a bitstream provenance - * message generated by the InstallItemService. - * - * @param context the current DSpace context, which provides details about the current user - * and authorization information. - * @param item the DSpace item to which the provenance metadata should be added. - * @param msg a custom message describing the action taken on the item. - * @throws SQLException if there is a database access error while updating the item. - * @throws AuthorizeException if the current user is not authorized to add metadata to the item. - */ - protected void addProvenanceMetadata(Context context, Item item, String msg) - throws SQLException, AuthorizeException { - String timestamp = DCDate.getCurrent().toString(); - EPerson e = context.getCurrentUser(); - StringBuilder prov = new StringBuilder(); - prov.append("Item ").append(msg).append(" by ") - .append(e.getFullName()).append(" (").append(e.getEmail()).append(") on ") - .append(timestamp).append("\n"); - prov.append(installItemService.getBitstreamProvenanceMessage(context, item)); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", prov.toString()); - //Update item in DB - itemService.update(context, item); - } - /** * Determines whether or not this Patch Operation can do this patch (Object of operation and path gets checked) * @param objectToMatch Object whose class must be instance of type object 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..a7b72c058b9d --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProvenanceServiceIT.java @@ -0,0 +1,192 @@ +/** + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.dspace.app.rest.matcher.CollectionMatcher; +import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.hamcrest.Matchers; +import org.hamcrest.core.StringContains; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MvcResult; + +import javax.ws.rs.core.MediaType; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist; +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.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ProvenanceServiceIT extends AbstractControllerIntegrationTest { + static String PROVENANCE = "dc.description.provenance"; + private Collection collection; + private ObjectMapper objectMapper = new ObjectMapper(); + private JsonNode suite; + + @Autowired + private ItemService itemService; + + @Before + public void setup() throws Exception { + suite = objectMapper.readTree(getClass().getResourceAsStream("provenance-patch-suite.json")); + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + collection = CollectionBuilder.createCollection(context, parentCommunity).build(); + } + + private JsonNode preprocessingProvenance(String responseBody) throws JsonProcessingException { + //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); + return responseJson; + } + return null; + } + + private Item createItem() { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .makeUnDiscoverable() + .build(); + context.restoreAuthSystemState(); + return item; + } + + private void responseCheck(JsonNode response, String respKey) { + JsonNode expectedSubStr = suite.get(respKey); + JsonNode responseMetadata = response.get("metadata").get("dc.description.provenance"); + for (JsonNode expNode : expectedSubStr) { + boolean contains = false; + for (JsonNode node : responseMetadata) { + if (node.get("value").asText().contains(expNode.asText())) { + contains = true; + break; + } + } + if (!contains) { + Assert.fail("Metadata provenance do not contain expected data: " + expNode.asText()); + } + } + } + + private void itemCheck(Item item, String respKey) { + String expectedSubStr = suite.get(respKey).asText(); + List metadata = item.getMetadata(); + boolean contain = false; + for (MetadataValue value : metadata) { + if (!Objects.equals(value.getMetadataField().toString(), "dc_description_provenance")) { + continue; + } + if (value.getValue().contains(expectedSubStr)) { + contain = true; + break; + } + } + if (!contain) { + Assert.fail("Metadata provenance do not contain expected data: " + expectedSubStr); + } + } + + @Test + public void makeDsicoverableTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + Item item = createItem(); +// List ops = new ArrayList(); +// ReplaceOperation replaceOperation = new ReplaceOperation("/discoverable", true); +// ops.add(replaceOperation); +// String patchBody = getPatchContent(ops); + + // make discoverable + MvcResult mvcResult = getClient(token).perform(patch("/api/core/items/" + item.getID() + + "/discoverable") +// .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))) + .andReturn(); + responseCheck(Objects.requireNonNull(preprocessingProvenance(mvcResult.getResponse().getContentAsString())), + "discoverable"); + } + + @Test + public void mappedCollection() throws Exception { + context.turnOffAuthorisationSystem(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item = createItem(); + context.restoreAuthSystemState(); + + 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/" + col1.getID() + "\n" + ) + ); + + getClient().perform(get("/api/core/items/" + item.getID() + "/mappedCollections")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.mappedCollections", Matchers.containsInAnyOrder( + CollectionMatcher.matchCollectionEntry("Collection 1", col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/items"))) + ; + itemCheck(itemService.find(context, item.getID()), "mapped"); + } +} diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/provenance-patch-suite.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/provenance-patch-suite.json new file mode 100644 index 000000000000..812099d9bc27 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/provenance-patch-suite.json @@ -0,0 +1,5 @@ +{ + "discoverable": "Item was made discoverable by first (admin) last (admin) (admin@email.com) on \nNo. of bitstreams: 0\nItem was in collections:\n", + "mapped": "was mapped to collection" +} +