Skip to content

Commit cc2d79b

Browse files
authored
Merge pull request DSpace#11068 from 4Science/DURACOM-383-BatchDeletingOfDSpaceObjects
REST: Performance issue when deleting Communities, Collections, or Items with many Bitstreams
2 parents 0d9d0ce + 435ed15 commit cc2d79b

File tree

14 files changed

+1492
-125
lines changed

14 files changed

+1492
-125
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.content;
9+
10+
/**
11+
* Exception thrown when a virtual metadata type is invalid or not supported.
12+
*
13+
* @author Mykhaylo Boychuk ([email protected])
14+
*/
15+
public class BadVirtualMetadataTypeException extends Exception {
16+
17+
public BadVirtualMetadataTypeException() {
18+
super();
19+
}
20+
21+
public BadVirtualMetadataTypeException(String s, Throwable t) {
22+
super(s, t);
23+
}
24+
25+
public BadVirtualMetadataTypeException(String s) {
26+
super(s);
27+
}
28+
29+
public BadVirtualMetadataTypeException(Throwable t) {
30+
super(t);
31+
}
32+
33+
}

dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
import java.util.Collections;
1313
import java.util.HashMap;
1414
import java.util.List;
15+
import java.util.Objects;
1516
import java.util.UUID;
1617
import java.util.stream.Collectors;
1718
import java.util.stream.Stream;
1819

1920
import org.apache.commons.collections4.CollectionUtils;
21+
import org.apache.commons.lang3.StringUtils;
2022
import org.apache.commons.lang3.Strings;
2123
import org.apache.logging.log4j.LogManager;
2224
import org.apache.logging.log4j.Logger;
@@ -1116,4 +1118,84 @@ public int countByItemRelationshipTypeAndRelatedList(Context context, UUID focus
11161118
return relationshipDAO
11171119
.countByItemAndRelationshipTypeAndList(context, focusUUID, relationshipType, items, isLeft);
11181120
}
1121+
1122+
@Override
1123+
public void deleteMultipleRelationshipsCopyVirtualMetadata(Context context, String[] copyVirtual, Item item)
1124+
throws SQLException, AuthorizeException, BadVirtualMetadataTypeException {
1125+
1126+
if (copyVirtual == null || copyVirtual.length == 0) {
1127+
// Don't delete nor copy any metadata here if the "copyVirtualMetadata" parameter wasn't passed. The
1128+
// relationships not deleted in this method will be deleted implicitly by the this.delete() method
1129+
// without copying the metadata anyway.
1130+
return;
1131+
}
1132+
1133+
if (Objects.deepEquals(copyVirtual, COPYVIRTUAL_ALL)) {
1134+
// Option 1: Copy all virtual metadata of this item to its related items. Iterate over all of the item's
1135+
// relationships and copy their data.
1136+
for (Relationship relationship : findByItem(context, item)) {
1137+
deleteRelationshipCopyVirtualMetadata(context, item, relationship);
1138+
}
1139+
} else if (Objects.deepEquals(copyVirtual, COPYVIRTUAL_CONFIGURED)) {
1140+
// Option 2: Use a configuration value to determine if virtual metadata needs to be copied. Iterate over all
1141+
// of the item's relationships and copy their data depending on the
1142+
// configuration.
1143+
for (Relationship relationship : findByItem(context, item)) {
1144+
boolean copyToLeft = relationship.getRelationshipType().isCopyToLeft();
1145+
boolean copyToRight = relationship.getRelationshipType().isCopyToRight();
1146+
if (relationship.getLeftItem().getID().equals(item.getID())) {
1147+
copyToLeft = false;
1148+
} else {
1149+
copyToRight = false;
1150+
}
1151+
forceDelete(context, relationship, copyToLeft, copyToRight);
1152+
}
1153+
} else {
1154+
// Option 3: Copy the virtual metadata of selected types of this item to its related items. The copyVirtual
1155+
// array should only contain numeric values at this point. These values are used to select the
1156+
// types. Iterate over all selected types and copy the corresponding values to this item's
1157+
// relatives.
1158+
List<Integer> relationshipIds = parseVirtualMetadataTypes(copyVirtual);
1159+
for (Integer relationshipId : relationshipIds) {
1160+
RelationshipType relationshipType = relationshipTypeService.find(context, relationshipId);
1161+
for (Relationship relationship : findByItemAndRelationshipType(context, item, relationshipType)) {
1162+
deleteRelationshipCopyVirtualMetadata(context, item, relationship);
1163+
}
1164+
}
1165+
}
1166+
}
1167+
1168+
private List<Integer> parseVirtualMetadataTypes(String[] copyVirtual) throws BadVirtualMetadataTypeException {
1169+
List<Integer> types = new ArrayList<>();
1170+
for (String typeString : copyVirtual) {
1171+
if (!StringUtils.isNumeric(typeString)) {
1172+
var message = String.format("parameter %s should only contain a single value '%s', '%s' or a list of " +
1173+
"numbers.", REQUESTPARAMETER_COPYVIRTUALMETADATA, COPYVIRTUAL_ALL[0], COPYVIRTUAL_CONFIGURED[0]);
1174+
throw new BadVirtualMetadataTypeException(message);
1175+
}
1176+
types.add(Integer.parseInt(typeString));
1177+
}
1178+
return types;
1179+
}
1180+
1181+
/**
1182+
* Deletes the relationship while copying the virtual metadata to the item which is **NOT** deleted
1183+
*
1184+
* @param itemToDelete The item to be deleted
1185+
* @param relationshipToDelete The relationship to be deleted
1186+
*/
1187+
private void deleteRelationshipCopyVirtualMetadata(Context context, Item itemToDelete,
1188+
Relationship relationshipToDelete) throws SQLException, AuthorizeException {
1189+
1190+
boolean copyToLeft = relationshipToDelete.getRightItem().equals(itemToDelete);
1191+
boolean copyToRight = relationshipToDelete.getLeftItem().equals(itemToDelete);
1192+
1193+
if (copyToLeft && copyToRight) {
1194+
//The item has a relationship with itself. Copying metadata is useless since the item will be deleted
1195+
copyToLeft = false;
1196+
copyToRight = false;
1197+
}
1198+
forceDelete(context, relationshipToDelete, copyToLeft, copyToRight);
1199+
}
1200+
11191201
}

dspace-api/src/main/java/org/dspace/content/service/RelationshipService.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.UUID;
1313

1414
import org.dspace.authorize.AuthorizeException;
15+
import org.dspace.content.BadVirtualMetadataTypeException;
1516
import org.dspace.content.Item;
1617
import org.dspace.content.Relationship;
1718
import org.dspace.content.Relationship.LatestVersionStatus;
@@ -25,6 +26,10 @@
2526
*/
2627
public interface RelationshipService extends DSpaceCRUDService<Relationship> {
2728

29+
public static final String[] COPYVIRTUAL_ALL = {"all"};
30+
public static final String[] COPYVIRTUAL_CONFIGURED = {"configured"};
31+
public static final String REQUESTPARAMETER_COPYVIRTUALMETADATA = "copyVirtualMetadata";
32+
2833
/**
2934
* Retrieves the list of Relationships currently in the system for which the given Item is either
3035
* a leftItem or a rightItem object
@@ -525,4 +530,15 @@ public List<Relationship> findByItemRelationshipTypeAndRelatedList(Context conte
525530
public int countByItemRelationshipTypeAndRelatedList(Context context, UUID focusUUID,
526531
RelationshipType relationshipType, List<UUID> items, boolean isLeft) throws SQLException;
527532

533+
/**
534+
* Deletes relationships of an item which need virtual metadata to be copied to actual metadata
535+
* This ensures a delete call is used which can copy the metadata prior to deleting the item
536+
*
537+
* @param context The relevant DSpace context
538+
* @param copyVirtual The value(s) of the copyVirtualMetadata parameter
539+
* @param item The item to be deleted
540+
*/
541+
public void deleteMultipleRelationshipsCopyVirtualMetadata(Context context, String[] copyVirtual, Item item)
542+
throws SQLException, AuthorizeException, BadVirtualMetadataTypeException;
543+
528544
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.deletion.process;
9+
10+
import java.sql.SQLException;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Optional;
14+
import java.util.UUID;
15+
16+
import org.apache.commons.cli.ParseException;
17+
import org.dspace.authorize.AuthorizeException;
18+
import org.dspace.authorize.factory.AuthorizeServiceFactory;
19+
import org.dspace.authorize.service.AuthorizeService;
20+
import org.dspace.content.Collection;
21+
import org.dspace.content.Community;
22+
import org.dspace.content.DSpaceObject;
23+
import org.dspace.content.Item;
24+
import org.dspace.content.factory.ContentServiceFactory;
25+
import org.dspace.content.service.CollectionService;
26+
import org.dspace.content.service.CommunityService;
27+
import org.dspace.content.service.ItemService;
28+
import org.dspace.core.Constants;
29+
import org.dspace.core.Context;
30+
import org.dspace.deletion.process.strategies.DSpaceObjectDeletionStrategy;
31+
import org.dspace.eperson.EPerson;
32+
import org.dspace.eperson.factory.EPersonServiceFactory;
33+
import org.dspace.handle.factory.HandleServiceFactory;
34+
import org.dspace.handle.service.HandleService;
35+
import org.dspace.kernel.ServiceManager;
36+
import org.dspace.scripts.DSpaceRunnable;
37+
import org.dspace.utils.DSpace;
38+
39+
/**
40+
* Batch process for deleting DSpace objects (Item, Collection, Community).
41+
* This class orchestrates the deletion process, delegating the actual deletion logic
42+
* to a strategy registry that selects the appropriate strategy for each DSpaceObject type.
43+
*
44+
* @author Mykhaylo Boychuk ([email protected])
45+
*/
46+
public class DSpaceObjectDeletionProcess
47+
extends DSpaceRunnable<DSpaceObjectDeletionProcessScriptConfiguration<DSpaceObjectDeletionProcess>> {
48+
49+
public static final String OBJECT_DELETION_SCRIPT = "object-deletion";
50+
51+
private ItemService itemService;
52+
private HandleService handleService;
53+
private CommunityService communityService;
54+
private AuthorizeService authorizeService;
55+
private CollectionService collectionService;
56+
57+
private String id;
58+
private Context context;
59+
private String[] copyVirtualMetadata;
60+
private List<DSpaceObjectDeletionStrategy> deletionStrategies = new ArrayList<>();
61+
62+
@Override
63+
public void setup() throws ParseException {
64+
ServiceManager serviceManager = new DSpace().getServiceManager();
65+
itemService = ContentServiceFactory.getInstance().getItemService();
66+
handleService = HandleServiceFactory.getInstance().getHandleService();
67+
communityService = ContentServiceFactory.getInstance().getCommunityService();
68+
collectionService = ContentServiceFactory.getInstance().getCollectionService();
69+
authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
70+
deletionStrategies.addAll(serviceManager.getServicesByType(DSpaceObjectDeletionStrategy.class));
71+
72+
parseCommandLineOptions();
73+
}
74+
75+
/**
76+
* Parses and validates command line options.
77+
*/
78+
private void parseCommandLineOptions() {
79+
this.id = commandLine.getOptionValue('i');
80+
this.copyVirtualMetadata = commandLine.hasOption('c') ? parseCopyVirtualMetadataOption() : new String[0];
81+
}
82+
83+
private String[] parseCopyVirtualMetadataOption() {
84+
String value = commandLine.getOptionValue('c');
85+
if (value.contains(",")) {
86+
return value.split(",");
87+
}
88+
return new String[] { value };
89+
}
90+
91+
@Override
92+
public void internalRun() throws Exception {
93+
assignCurrentUserInContext();
94+
Optional<DSpaceObject> dSpaceObjectOptional = resolveDSpaceObject(this.id);
95+
96+
if (dSpaceObjectOptional.isEmpty()) {
97+
var error = String.format("DSpaceObject for provided identifier:%s doesn't exist!", this.id);
98+
throw new IllegalArgumentException(error);
99+
}
100+
101+
DSpaceObject dso = dSpaceObjectOptional.get();
102+
103+
if (!authorizeService.isAdmin(context, dso)) {
104+
throw new AuthorizeException("Current user is not eligible to execute script: " + OBJECT_DELETION_SCRIPT);
105+
}
106+
107+
var info = "Performing deletion of DSpaceObject (and all child objects) for type=%s and uuid=%s";
108+
handler.logInfo(String.format(info, Constants.typeText[dso.getType()], dso.getID().toString()));
109+
getStrategy(dso).delete(this.context, dso, this.copyVirtualMetadata);
110+
handler.logInfo("Deletion completed!");
111+
}
112+
113+
private DSpaceObjectDeletionStrategy getStrategy(DSpaceObject dso) {
114+
var error = "No strategy for type:" + dso.getType();
115+
return deletionStrategies.stream()
116+
.filter(s -> s.supports(dso))
117+
.findFirst()
118+
.orElseThrow(() -> new IllegalArgumentException(error));
119+
}
120+
121+
private void assignCurrentUserInContext() throws SQLException {
122+
this.context = new Context();
123+
UUID uuid = getEpersonIdentifier();
124+
if (uuid != null) {
125+
EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid);
126+
context.setCurrentUser(ePerson);
127+
}
128+
}
129+
130+
/**
131+
* Resolves the identifier (Item, Collection, or Community).
132+
*
133+
* @param identifier The UUID or handle of the DSpace object.
134+
* @return An Optional containing the DSpaceObject if found.
135+
* @throws SQLException If database error occurs.
136+
*/
137+
private Optional<DSpaceObject> resolveDSpaceObject(String identifier) throws SQLException {
138+
UUID uuid = null;
139+
try {
140+
uuid = UUID.fromString(identifier);
141+
} catch (Exception e) {
142+
// It's not a UUID, proceed to treat it as a handle.
143+
}
144+
145+
if (uuid != null) {
146+
Item item = itemService.find(context, uuid);
147+
if (item != null) {
148+
return Optional.of(item);
149+
}
150+
Community community = communityService.find(context, uuid);
151+
if (community != null) {
152+
return Optional.of(community);
153+
}
154+
Collection collection = collectionService.find(context, uuid);
155+
if (collection != null) {
156+
return Optional.of(collection);
157+
}
158+
}
159+
DSpaceObject dso = handleService.resolveToObject(context, identifier);
160+
return dso != null ? Optional.of(dso) : Optional.empty();
161+
}
162+
163+
@Override
164+
public DSpaceObjectDeletionProcessScriptConfiguration<DSpaceObjectDeletionProcess> getScriptConfiguration() {
165+
ServiceManager sm = new DSpace().getServiceManager();
166+
return sm.getServiceByName(OBJECT_DELETION_SCRIPT, DSpaceObjectDeletionProcessScriptConfiguration.class);
167+
}
168+
169+
}

0 commit comments

Comments
 (0)