diff --git a/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinker.java b/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinker.java new file mode 100644 index 000000000000..ef4dacc90253 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinker.java @@ -0,0 +1,383 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.commons.cli.ParseException; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.IdentifierNotFoundException; +import org.dspace.identifier.IdentifierNotResolvableException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.IdentifierService; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.dspace.utils.DSpace; +import org.dspace.versioning.Version; +import org.dspace.versioning.VersionHistory; +import org.dspace.versioning.factory.VersionServiceFactory; +import org.dspace.versioning.service.VersionHistoryService; +import org.dspace.versioning.service.VersioningService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This script allows to link two items into the versioning relationship, + * where the second item becomes the next version of the first item. + * + * @author Milan Kuchtiak + */ +public class ItemVersionLinker extends DSpaceRunnable { + + private static final Logger log = LoggerFactory.getLogger(ItemVersionLinker.class); + private boolean help = false; + private boolean link = false; + private String previousItemID; + private String itemID; + private String ePersonEmail; + private VersioningService versioningService; + private VersionHistoryService versionHistoryService; + private ItemService itemService; + private EPersonService ePersonService; + private IdentifierService identifierService; + private AuthorizeService authorizeService; + + /** + * This method will return the Configuration that the implementing DSpaceRunnable uses + * + * @return The {@link ScriptConfiguration} that this implementing DspaceRunnable uses + */ + @Override + public ItemVersionLinkerConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("item-version-linker", + ItemVersionLinkerConfiguration.class); + } + + /** + * This method has to be included in every script and handles the setup of the script by parsing the CommandLine + * and setting the variables. + * + * @throws ParseException If something goes wrong + */ + @Override + public void setup() throws ParseException { + log.debug("Setting up {}", ItemVersionLinker.class.getName()); + + link = commandLine.hasOption("l"); + boolean unlink = commandLine.hasOption("u"); + + if (commandLine.hasOption("h") || (link && unlink) || (!link && !unlink)) { + help = true; + return; + } + + if (!commandLine.hasOption("i")) { + help = true; + return; + } + + if (link && !commandLine.hasOption("p")) { + help = true; + return; + } + + if (commandLine.hasOption("e")) { + ePersonEmail = commandLine.getOptionValue("e"); + } + + versioningService = VersionServiceFactory.getInstance().getVersionService(); + versionHistoryService = VersionServiceFactory.getInstance().getVersionHistoryService(); + itemService = ContentServiceFactory.getInstance().getItemService(); + ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + identifierService = IdentifierServiceFactory.getInstance().getIdentifierService(); + authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + } + + /** + * This method has to be included in every script and this will be the main execution block for the script that'll + * contain all the logic needed + * + * @throws Exception If something goes wrong + */ + @Override + public void internalRun() throws Exception { + log.debug("Running {}", ItemVersionLinker.class.getName()); + if (help) { + printHelp(); + return; + } + + try (Context context = new Context()) { + EPerson ePerson = getEperson(context); + if (ePerson == null) { + throw new RuntimeException("Only authenticated user can run the script."); + } + context.setCurrentUser(ePerson); + + if (ePersonEmail != null && !authorizeService.isAdmin(context)) { + handler.logError("Only admin user can run the script."); + return; + } + + itemID = commandLine.getOptionValue("i"); + Item item = findItem(context, itemID); + + if (item == null) { + throw new IllegalArgumentException(String.format("Item '%s' not found.", itemID)); + } + + if (link) { + previousItemID = commandLine.getOptionValue("p"); + Item previousItem = findItem(context, previousItemID); + if (previousItem == null) { + throw new IllegalArgumentException(String.format("Previous item '%s' not found.", previousItemID)); + } + linkItems(context, previousItem, item); + } else { + unlinkLastItem(context, item); + } + context.complete(); + } + } + + /** + * Link item with the previous item into the versioning relationship. + * + * @param context + * @param previousItem + * @param item + * @throws SQLException + * @throws AuthorizeException + */ + private void linkItems(Context context, Item previousItem, Item item) throws SQLException, AuthorizeException { + if (previousItem.getID().equals(item.getID())) { + handler.logError("Cannot create versioning relationship between the same item."); + return; + } + + if (itemService.isInProgressSubmission(context, previousItem) || + itemService.isInProgressSubmission(context, item)) { + // this script is intended to work only with archived items + handler.logError("Both items must be archived to create versioning relationship."); + return; + } + + Version previousVersion = versioningService.getVersion(context, previousItem); + + if (previousVersion != null && !isLatestVersion(context, previousVersion)) { + handler.logError(String.format("Previous item '%s' is already part of existing versioning history, " + + "and its version is not the latest version in that history.", + previousItemID)); + return; + } + + Version secondVersion = versioningService.getVersion(context, item); + if (secondVersion != null) { + // we don't allow to link item that is already part of some other versioning history + handler.logError(String.format("The item '%s' is already part of other versioning history.", itemID)); + return; + } + + String previousItemName = previousItem.getName(); + + String previousItemHandleRef = getHandleRef(previousItem); + if (previousItemHandleRef == null) { + handler.logError(getNoHandleMessage(previousItemID)); + return; + } + + String itemHandleRef = getHandleRef(item); + if (itemHandleRef == null) { + handler.logError(getNoHandleMessage(itemID)); + return; + } + + handler.logInfo(String.format("Creating versioning relationship between '%s' and '%s' items.", + previousItemID, itemID)); + + int newVersionNumber; + if (previousVersion != null) { + // create new version of item in existing versioning history + VersionHistory history = previousVersion.getVersionHistory(); + newVersionNumber = previousVersion.getVersionNumber() + 1; + versioningService.createNewVersion(context, history, item, + "Linked as the next version of " + previousItemName, new Date(), newVersionNumber); + } else { + // create new versioning history for the items + VersionHistory history = versionHistoryService.create(context); + versioningService.createNewVersion(context, history, previousItem, + "The first version of " + previousItemName, new Date(), 1); + versioningService.createNewVersion(context, history, item, + "Linked as the next version of " + previousItemName, new Date(), 2); + newVersionNumber = 2; + } + + itemService.addMetadata(context, previousItem, "dc", "relation", "isreplacedby", null, itemHandleRef); + + // remove "dc.relation.replaces" metadata, if any exists + itemService.clearMetadata(context, item, "dc", "relation", "replaces", Item.ANY); + itemService.addMetadata(context, item, "dc", "relation", "replaces", null, previousItemHandleRef); + + handler.logInfo(String.format("Item '%s' has become a new version (version %d) of item '%s'.", + itemID, newVersionNumber, previousItemID)); + } + + private void unlinkLastItem(Context context, Item item) throws SQLException, AuthorizeException { + Version version = versioningService.getVersion(context, item); + if (version == null) { + handler.logError(String.format("The item '%s', to be unlinked, is not part of any versioning history.", + itemID)); + return; + } + + if (!isLatestVersion(context, version)) { + handler.logError("Can unlink only the item whose version is the latest version in the versioning history."); + return; + } + + String itemHandleRef = getHandleRef(item); + if (itemHandleRef == null) { + handler.logError(getNoHandleMessage(itemID)); + return; + } + + handler.logInfo(String.format("Going to unlink item '%s' from the versioning history.", + itemID)); + + // remove "dc.relation.replaces" metadata, if any exists + itemService.clearMetadata(context, item, "dc", "relation", "replaces", Item.ANY); + + VersionHistory versionHistory = version.getVersionHistory(); + Version previousVersion = versionHistoryService.getPrevious(context, version.getVersionHistory(), version); + + // remove the version + versioningService.deleteVersion(context, version); + handler.logInfo(String.format("Item '%s' unlinked successfully.", itemID)); + + if (previousVersion != null) { + // from the previous item, remove the "dc.relation.isreplacedby" metadata, related to item being unlinked + List metadataValuesToRemove = + itemService.getMetadata(previousVersion.getItem(), "dc", "relation", "isreplacedby", Item.ANY) + .stream().filter(metadataValue -> itemHandleRef.equals(metadataValue.getValue())) + .collect(Collectors.toList()); + + if (!metadataValuesToRemove.isEmpty()) { + itemService.removeMetadataValues(context, previousVersion.getItem(), metadataValuesToRemove); + } + + if (isFirstVersion(context, versionHistory, previousVersion)) { + // if the previous version is the first version, we need to remove the version + // and the full versioning history as well + versioningService.deleteVersion(context, previousVersion); + versionHistoryService.delete(context, versionHistory); + + // guess identifier type for previous item (only for logging) + String previousItemID = isUUID(itemID) ? + previousVersion.getItem().getID().toString() : getHandle(previousVersion.getItem()); + + handler.logInfo(String.format("The previous item '%s' was the first version of the '%s' item, " + + "so the full versioning history associated with the items was removed as well.", + previousItemID, itemID)); + } + } else { + // there is no previous version, so we need to remove the full versioning history as well + versionHistoryService.delete(context, versionHistory); + handler.logInfo(String.format("The item '%s' had no previous version in the versioning history, " + + "so the full versioning history associated with the item was removed as well.", itemID)); + } + } + + private Item findItem(Context context, String itemId) throws SQLException { + try { + return itemService.find(context, UUID.fromString(itemId)); + } catch (IllegalArgumentException ex) { + try { + DSpaceObject dso = identifierService.resolve(context, itemId); + if (dso instanceof Item) { + return (Item) dso; + } else { + throw new IllegalArgumentException(String.format("Unable to resolve '%s' identifier.", itemId)); + } + } catch (IdentifierNotFoundException | IdentifierNotResolvableException iex) { + throw new IllegalArgumentException(iex); + } + } + } + + private EPerson getEperson(Context context) throws SQLException { + if (ePersonEmail != null) { + return ePersonService.findByEmail(context, ePersonEmail); + } else { + UUID ePersonIdentifier = getEpersonIdentifier(); + return ePersonIdentifier == null ? null : ePersonService.find(context, ePersonIdentifier); + } + } + + private boolean isLatestVersion(Context context, Version version) throws SQLException { + return versionHistoryService.isLastVersion(context, version.getVersionHistory(), version); + } + + private boolean isFirstVersion(Context context, VersionHistory versionHistory, Version version) + throws SQLException { + return versionHistoryService.isFirstVersion(context, versionHistory, version); + } + + private boolean isUUID(String itemID) { + try { + UUID.fromString(itemID); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private String getHandleRef(Item item) { + return itemService.getMetadata(item, "dc", "identifier", "uri", Item.ANY) + .stream() + .findFirst() + .map(MetadataValue::getValue) + .orElse(null); + } + + private String getHandle(Item item) { + // extract handle from handle reference + // handleRef cannot be null here as this method is called only after checking for null + String handleRef = Objects.requireNonNull(getHandleRef(item)); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); + String handlePrefix = handleService.getCanonicalPrefix(); + if (handleRef.startsWith(handlePrefix)) { + return handleRef.substring(handlePrefix.length()); + } else { + return ""; + } + } + + private static String getNoHandleMessage(String itemID) { + return String.format("Item '%s' has no handle assigned.", itemID); + } + +} + diff --git a/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinkerConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinkerConfiguration.java new file mode 100644 index 000000000000..a924d3de8ad0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/administer/ItemVersionLinkerConfiguration.java @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import org.apache.commons.cli.Options; +import org.dspace.scripts.configuration.ScriptConfiguration; + +/** + * The {@link ScriptConfiguration} for the {@link ItemVersionLinker} script. + * + * @author Milan Kuchtiak + */ +public class ItemVersionLinkerConfiguration extends ScriptConfiguration { + + private Class dspaceRunnableClass; + + /** + * Generic getter for the dspaceRunnableClass + * + * @return the dspaceRunnableClass value of this ScriptConfiguration + */ + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + /** + * Generic setter for the dspaceRunnableClass + * + * @param dspaceRunnableClass The dspaceRunnableClass to be set for this ScriptConfiguration + */ + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + /** + * The getter for the options of the Script + * + * @return the options value of this ScriptConfiguration + */ + @Override + public Options getOptions() { + if (options == null) { + + Options options = new Options(); + + options.addOption("h", "help", false, "help"); + + options.addOption("l", "link", false, "link item with the previous item"); + + options.addOption("u", "unlink", false, "unlink item from the previous item in version history"); + + options.addOption("p", "previous", true, + "item handle, or UUID, of the previous(left) item that is intended to be linked with the (right)" + + " item (only required for link option)"); + + options.addOption("i", "item", true, + "item handle, or UUID, of the (right) item that is intended to be linked/unlinked with/from the " + + "previous item (required for both link and unlink options)"); + options.getOption("i").setRequired(true); + + options.addOption("e", "eperson", true, "ePerson email"); + options.getOption("e").setRequired(false); + + super.options = options; + } + return options; + } +} diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java index 493861df1c60..87a865f03db7 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java @@ -74,7 +74,7 @@ public void update(Context context, List versionHistories) throw @Override public void delete(Context context, VersionHistory versionHistory) throws SQLException, AuthorizeException { - versionHistoryDAO.delete(context, new VersionHistory()); + versionHistoryDAO.delete(context, versionHistory); } // LIST order: descending diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java index b6f708b50066..9394d72498bc 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersioningServiceImpl.java @@ -266,4 +266,8 @@ public int countVersionsByHistoryWithItem(Context context, VersionHistory versio return versionDAO.countVersionsByHistoryWithItem(context, versionHistory); } + @Override + public void deleteVersion(Context c, Version version) throws SQLException { + versionDAO.delete(c, version); + } } diff --git a/dspace-api/src/main/java/org/dspace/versioning/service/VersioningService.java b/dspace-api/src/main/java/org/dspace/versioning/service/VersioningService.java index 2f6df5b732f7..a34a8e6bd43e 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/service/VersioningService.java +++ b/dspace-api/src/main/java/org/dspace/versioning/service/VersioningService.java @@ -45,7 +45,7 @@ public interface VersioningService { * To keep version numbers stable we do not delete versions, we do only set * the item, date, summary and eperson null. This methods returns only those * versions that have an item assigned. - * + * * @param c The relevant DSpace Context. * @param vh Version history * @param offset The position of the first result to return @@ -79,6 +79,16 @@ List getVersionsByHistoryWithItems(Context c, VersionHistory vh, int of Version createNewVersion(Context context, VersionHistory history, Item item, String summary, Date date, int versionNumber); + /** + * This method deletes the version associated with the item from the versioning history, + * but unlike the {@link #delete} method this doesn't delete the entire item. + * + * @param c context + * @param version version + * @throws SQLException if database error + */ + void deleteVersion(Context c, Version version) throws SQLException; + /** * Update the Version * @@ -94,7 +104,7 @@ Version createNewVersion(Context context, VersionHistory history, Item item, Str * remove a version we set the item, date, summary and eperson null. This * method returns only versions that aren't soft deleted and have items * assigned. - * + * * @param context The relevant DSpace Context. * @param versionHistory Version history * @return Total versions of an version history that have items assigned. diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index 95d34d77c7cc..aaecc074e0bb 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -39,7 +39,7 @@ - + @@ -106,6 +106,11 @@ + + + + + diff --git a/dspace-api/src/test/java/org/dspace/administer/ItemVersionLinkerIT.java b/dspace-api/src/test/java/org/dspace/administer/ItemVersionLinkerIT.java new file mode 100644 index 000000000000..a910c992e186 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/administer/ItemVersionLinkerIT.java @@ -0,0 +1,395 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.administer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.launcher.ScriptLauncher; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; +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.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.eperson.EPerson; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.versioning.Version; +import org.dspace.versioning.VersionHistory; +import org.dspace.versioning.factory.VersionServiceFactory; +import org.dspace.versioning.service.VersionHistoryService; +import org.dspace.versioning.service.VersioningService; +import org.junit.Before; +import org.junit.Test; + +public class ItemVersionLinkerIT extends AbstractIntegrationTestWithDatabase { + + private TestDSpaceRunnableHandler testDSpaceRunnableHandler; + private Collection collection; + private Item item1; + private Item item2; + private Item item3; + + private ItemService itemService; + private VersioningService versioningService; + private VersionHistoryService versionHistoryService; + private HandleService handleService; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + context.setCurrentUser(admin); + Community community = CommunityBuilder.createCommunity(context).build(); + collection = CollectionBuilder.createCollection(context, community) + .withSubmitterGroup(eperson) + .build(); + item1 = ItemBuilder.createItem(context, collection).withTitle("Item 1").build(); + item2 = ItemBuilder.createItem(context, collection).withTitle("Item 2").build(); + item3 = ItemBuilder.createItem(context, collection).withTitle("Item 3").build(); + itemService = ContentServiceFactory.getInstance().getItemService(); + versioningService = VersionServiceFactory.getInstance().getVersionService(); + versionHistoryService = VersionServiceFactory.getInstance().getVersionHistoryService(); + handleService = HandleServiceFactory.getInstance().getHandleService(); + testDSpaceRunnableHandler = createTestHandler(); + } + + @Test() + public void testLink() throws Exception { + // link item1 with item2 should pass + runScript(getLinkOptions(item1, item2, admin)); + assertLinkMessages(item1, item2, 2); + testDSpaceRunnableHandler.getInfoMessages().clear(); + + // linking item1 with item3 should fail since item1 is not the last version anymore + runScript(getLinkOptions(item1, item3, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(String.format("Previous item '%s' is already part of existing versioning history, " + + "and its version is not the latest version in that history.", item1.getID()), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // there is a limitation that an item that is going to be connected with previous item + // cannot be part of any versioning history (we don't support one item being part of two versioning histories) + // in this case, item2 is already in version history with item1 + runScript(getLinkOptions(item3, item2, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getLinkErrorMessagePartOfOtherVersionHistory(item2), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // linking item3 with item1 should fail (same as above) + runScript(getLinkOptions(item3, item1, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getLinkErrorMessagePartOfOtherVersionHistory(item1), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // linking item2 with item1 should fail also (cyclic linking) + runScript(getLinkOptions(item2, item1, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getLinkErrorMessagePartOfOtherVersionHistory(item1), getErrorMessage()); + } + + @Test() + public void testLink3Items() throws Exception { + // create version history with item1 and item2 + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + createNewVersion(versionHistory, item2, 2); + + // link item2 with item3 should pass + runScript(getLinkOptions(item2, item3, admin)); + assertLinkMessages(item2, item3, 3); + + Version v3 = versioningService.getVersion(context, item3); + assertEquals(v3.getVersionHistory(), versionHistory); + } + + @Test() + public void testLinkErrorNotAdmin() throws Exception { + runScript(getLinkOptions(item1, item2, eperson)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals("Only admin user can run the script.", getErrorMessage()); + } + + @Test() + public void testLinkErrorItemToItself() throws Exception { + runScript(getLinkOptions(item1, item1, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals("Cannot create versioning relationship between the same item.", getErrorMessage()); + } + + @Test() + public void testLinkItemNoHandle() throws Exception { + Item item4 = ItemBuilder.createItem(context, collection).withTitle("Item 4").build(); + itemService.clearMetadata(context, item4, "dc", "identifier", "uri", Item.ANY); + + // linking item1 with item4 should fail since item4 has no handle + runScript(getLinkOptions(item1, item4, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getNoHandleMessage(item4.getID()), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // linking item4 with item1 should also fail since item4 has no handle + runScript(getLinkOptions(item4, item1, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getNoHandleMessage(item4.getID()), getErrorMessage()); + } + + @Test() + public void testLinkErrorInvalidUuid() throws Exception { + runScript(new String[]{"item-version-linker", "-l", "-p", item1.getHandle(), + "-i", "invalid-uuid", "-e", admin.getEmail()}); + assertNotNull(testDSpaceRunnableHandler.getException()); + assertEquals("Unable to resolve 'invalid-uuid' identifier.", getExceptionMessage()); + } + + @Test() + public void testLinkErrorItemNotFound() throws Exception { + UUID randomUUID = UUID.randomUUID(); + runScript(new String[] { "item-version-linker", "-l", "-p", item1.getHandle(), + "-i", randomUUID.toString(), "-e", admin.getEmail() }); + assertNotNull(testDSpaceRunnableHandler.getException()); + assertEquals(String.format("Item '%s' not found.", randomUUID), getExceptionMessage()); + } + + @Test() + public void testUnlink() throws Exception { + // create version history with item1, item2 and item3 + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + createNewVersion(versionHistory, item2, 2); + createNewVersion(versionHistory, item3, 3); + + // unlinking item3 + runScript(getUnlinkOptions(item3, admin)); + assertUnlinkMessages(item2, item3, item3.getID()); + testDSpaceRunnableHandler.getInfoMessages().clear(); + + // unlinking item3 again should fail as item3 is not linked anymore + runScript(getUnlinkOptions(item3, admin)); + assertEquals(getUnlinkErrorMessageNotPartOfVersionHistory(item3), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // unlinking item1 should fail as item1 is not the latest version + runScript(getUnlinkOptions(item1, admin)); + assertEquals(getUnlinkErrorMessageNotLastItem(), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + } + + @Test() + public void testUnlinkLastItems() throws Exception { + // create version history with item1 and item2 + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + createNewVersion(versionHistory, item2, 2); + + // unlinking item2 (will unlink both item1 and item2 since item1 was the first version) + runScript(getUnlinkOptions(item2, admin)); + assertUnlinkMessagesLastItems(item1, item2, item1.getID(), item2.getID()); + testDSpaceRunnableHandler.getInfoMessages().clear(); + + // unlinking item2 again should fail + runScript(getUnlinkOptions(item2, admin)); + assertEquals(getUnlinkErrorMessageNotPartOfVersionHistory(item2), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + + // unlinking item1 should also fail since both items item1 and item2 were unlinked + // because item1 was the first item in the versioning history + runScript(getUnlinkOptions(item1, admin)); + assertEquals(getUnlinkErrorMessageNotPartOfVersionHistory(item1), getErrorMessage()); + + // check if version history was removed + assertNull(versionHistoryService.find(context, versionHistory.getID())); + } + + @Test() + public void testUnlinkLastItemsWithHandles() throws Exception { + // create version history with item1 and item2 + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + createNewVersion(versionHistory, item2, 2); + + // unlinking item2 (will unlink both item1 and item2 since item1 was the first version) + runScript(new String[] { "item-version-linker", "-u", "-i", item2.getHandle(), "-e", admin.getEmail() }); + assertUnlinkMessagesLastItems(item1, item2, item1.getHandle(), item2.getHandle()); + + // check if version history was removed + assertNull(versionHistoryService.find(context, versionHistory.getID())); + } + + @Test() + public void testUnlinkItemNoHandle() throws Exception { + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + createNewVersion(versionHistory, item2, 2); + + itemService.clearMetadata(context, item2, "dc", "identifier", "uri", Item.ANY); + + // unlinking item2 should fail since item2 has no handle + runScript(getUnlinkOptions(item2, admin)); + assertEquals(1, testDSpaceRunnableHandler.getErrorMessages().size()); + assertEquals(getNoHandleMessage(item2.getID()), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + } + + @Test() + public void testUnlinkSingleItemInHistory() throws Exception { + // create version history with one item only + VersionHistory versionHistory = versionHistoryService.create(context); + createNewVersion(versionHistory, item1, 1); + + // unlinking item1 should remove also the version history since item1 is the only item in that history + runScript(getUnlinkOptions(item1, admin)); + assertEquals(0, testDSpaceRunnableHandler.getErrorMessages().size()); + List infoMessages = testDSpaceRunnableHandler.getInfoMessages(); + assertEquals(3, infoMessages.size()); + assertEquals(getUnlinkStartMessage(item1.getID()), infoMessages.get(0)); + assertEquals(getUnlinkSuccessMessage(item1.getID()), infoMessages.get(1)); + assertEquals(String.format("The item '%s' had no previous version in the versioning history, " + + "so the full versioning history associated with the item was removed as well.", item1.getID()), + infoMessages.get(2)); + testDSpaceRunnableHandler.getInfoMessages().clear(); + + // check if version history was removed + assertNull(versionHistoryService.find(context, versionHistory.getID())); + + // unlinking item1 again should fail + runScript(getUnlinkOptions(item1, admin)); + assertEquals(getUnlinkErrorMessageNotPartOfVersionHistory(item1), getErrorMessage()); + testDSpaceRunnableHandler.getErrorMessages().clear(); + } + + private void assertLinkMessages(Item item1, Item item2, int version) throws SQLException { + assertEquals(0, testDSpaceRunnableHandler.getErrorMessages().size()); + List infoMessages = testDSpaceRunnableHandler.getInfoMessages(); + assertEquals(2, infoMessages.size()); + + assertEquals(String.format("Creating versioning relationship between '%s' and '%s' items.", + item1.getID(), item2.getID()), infoMessages.get(0)); + assertEquals(String.format("Item '%s' has become a new version (version %d) of item '%s'.", + item2.getID(), version, item1.getID()), infoMessages.get(1)); + + Version v1 = versioningService.getVersion(context, item1); + Version v2 = versioningService.getVersion(context, item2); + assertEquals(v1.getVersionHistory(), v2.getVersionHistory()); + assertTrue(v1.getVersionNumber() < v2.getVersionNumber()); + + // check dc.relation metadata added + List isReplacedBy = itemService.getMetadata(item1, "dc", "relation", "isreplacedby", null); + assertEquals(1, isReplacedBy.size()); + assertTrue(isReplacedBy.get(0).getValue().endsWith(item2.getHandle())); + + List replaces = itemService.getMetadata(item2, "dc", "relation", "replaces", null); + assertEquals(1, replaces.size()); + assertTrue(replaces.get(0).getValue().endsWith(item1.getHandle())); + } + + private void assertUnlinkMessages(Item item1, Item item2, Object item2ID) { + assertEquals(0, testDSpaceRunnableHandler.getErrorMessages().size()); + List infoMessages = testDSpaceRunnableHandler.getInfoMessages(); + assertTrue(infoMessages.size() >= 2); + + assertEquals(getUnlinkStartMessage(item2ID), infoMessages.get(0)); + assertEquals(getUnlinkSuccessMessage(item2ID), infoMessages.get(1)); + + // check dc.relation metadata removed + List isReplacedBy = itemService.getMetadata(item1, "dc", "relation", "isreplacedby", null); + assertEquals(0, isReplacedBy.size()); + + List replaces = itemService.getMetadata(item2, "dc", "relation", "replaces", null); + assertEquals(0, replaces.size()); + } + + private void assertUnlinkMessagesLastItems(Item item1, Item item2, Object item1ID, Object item2ID) { + assertUnlinkMessages(item1, item2, item2ID); + assertEquals(String.format("The previous item '%s' was the first version of the '%s' item, " + + "so the full versioning history associated with the items was removed as well.", + item1ID, item2ID), + testDSpaceRunnableHandler.getInfoMessages().get(2)); + } + + private static String getUnlinkStartMessage(Object itemID) { + return String.format("Going to unlink item '%s' from the versioning history.", itemID); + } + + private static String getUnlinkSuccessMessage(Object itemID) { + return String.format("Item '%s' unlinked successfully.", itemID); + } + + private static String getNoHandleMessage(Object itemID) { + return String.format("Item '%s' has no handle assigned.", itemID); + } + + private static String getLinkErrorMessagePartOfOtherVersionHistory(Item item) { + return String.format("The item '%s' is already part of other versioning history.", item.getID()); + } + + private static String getUnlinkErrorMessageNotPartOfVersionHistory(Item item) { + return String.format("The item '%s', to be unlinked, is not part of any versioning history.", item.getID()); + } + + private static String getUnlinkErrorMessageNotLastItem() { + return "Can unlink only the item whose version is the latest version in the versioning history."; + } + + private static String[] getLinkOptions(Item item1, Item item2, EPerson eperson) { + return new String[] { "item-version-linker", + "-l", "-p", item1.getID().toString(), "-i", item2.getID().toString(), "-e", eperson.getEmail() }; + } + + private static String[] getUnlinkOptions(Item item, EPerson eperson) { + return new String[] { "item-version-linker", "-u", "-i", item.getID().toString(), "-e", eperson.getEmail() }; + } + + private void runScript(String[] args) throws Exception { + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + } + + private TestDSpaceRunnableHandler createTestHandler() { + return new TestDSpaceRunnableHandler(); + } + + private String getErrorMessage() { + return testDSpaceRunnableHandler.getErrorMessages().get(0); + } + + private String getExceptionMessage() { + return testDSpaceRunnableHandler.getException().getMessage(); + } + + private void createNewVersion(VersionHistory versionHistory, Item item, int versionNumber) throws SQLException { + Version version = versioningService.createNewVersion(context, versionHistory, item, + "version " + versionNumber, new Date(), versionNumber); + if (!versionHistoryService.isFirstVersion(context, versionHistory, version)) { + Version previous = versionHistoryService.getPrevious(context, versionHistory, version); + Item previousItem = previous.getItem(); + + String previousItemHandleRef = handleService.getCanonicalForm(previousItem.getHandle()); + String secondItemHandleRef = handleService.getCanonicalForm(item.getHandle()); + + itemService.addMetadata(context, previousItem, "dc", "relation", "isreplacedby", null, + secondItemHandleRef); + + itemService.addMetadata(context, item, "dc", "relation", "replaces", null, + previousItemHandleRef); + } + } + +} \ No newline at end of file diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index 336692947a2b..994621e7c9fc 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -106,4 +106,9 @@ + + + + + diff --git a/dspace/config/spring/rest/scripts.xml b/dspace/config/spring/rest/scripts.xml index 5ae2bedd7d17..0cf4d1c49ddf 100644 --- a/dspace/config/spring/rest/scripts.xml +++ b/dspace/config/spring/rest/scripts.xml @@ -94,4 +94,9 @@ + + + + +