Merge pull request #9718 from 4Science/task/main/CST-14901

Handles versioning for ORCID publications.
This commit is contained in:
Tim Donohue
2025-01-22 14:50:06 -06:00
committed by GitHub
11 changed files with 374 additions and 59 deletions

View File

@@ -82,6 +82,9 @@ import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.qaevent.dao.QAEventsDAO;
import org.dspace.services.ConfigurationService;
import org.dspace.versioning.Version;
import org.dspace.versioning.VersionHistory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.service.VersioningService;
import org.dspace.workflow.WorkflowItemService;
import org.dspace.workflow.factory.WorkflowServiceFactory;
@@ -177,6 +180,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
@Autowired
private QAEventsDAO qaEventsDao;
@Autowired
private VersionHistoryService versionHistoryService;
protected ItemServiceImpl() {
}
@@ -1933,4 +1939,40 @@ prevent the generation of resource policy entry values with null dspace_object a
}
}
@Override
public boolean isLatestVersion(Context context, Item item) throws SQLException {
VersionHistory history = versionHistoryService.findByItem(context, item);
if (history == null) {
// not all items have a version history
// if an item does not have a version history, it is by definition the latest
// version
return true;
}
// start with the very latest version of the given item (may still be in
// workspace)
Version latestVersion = versionHistoryService.getLatestVersion(context, history);
// find the latest version of the given item that is archived
while (latestVersion != null && !latestVersion.getItem().isArchived()) {
latestVersion = versionHistoryService.getPrevious(context, history, latestVersion);
}
// could not find an archived version of the given item
if (latestVersion == null) {
// this scenario should never happen, but let's err on the side of showing too
// many items vs. to little
// (see discovery.xml, a lot of discovery configs filter out all items that are
// not the latest version)
return true;
}
// sanity check
assert latestVersion.getItem().isArchived();
return item.equals(latestVersion.getItem());
}
}

View File

@@ -1009,4 +1009,14 @@ public interface ItemService
*/
EntityType getEntityType(Context context, Item item) throws SQLException;
/**
* Check whether the given item is the latest version. If the latest item cannot
* be determined, because either the version history or the latest version is
* not present, assume the item is latest.
* @param context the DSpace context.
* @param item the item that should be checked.
* @return true if the item is the latest version, false otherwise.
*/
public boolean isLatestVersion(Context context, Item item) throws SQLException;
}

View File

@@ -67,8 +67,6 @@ import org.dspace.handle.service.HandleService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.MultiFormatDateParser;
import org.dspace.util.SolrUtils;
import org.dspace.versioning.Version;
import org.dspace.versioning.VersionHistory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService;
@@ -151,7 +149,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
doc.addField("withdrawn", item.isWithdrawn());
doc.addField("discoverable", item.isDiscoverable());
doc.addField("lastModified", SolrUtils.getDateFormatter().format(item.getLastModified()));
doc.addField("latestVersion", isLatestVersion(context, item));
doc.addField("latestVersion", itemService.isLatestVersion(context, item));
EPerson submitter = item.getSubmitter();
if (submitter != null && !(DSpaceServicesFactory.getInstance().getConfigurationService().getBooleanProperty(
@@ -177,43 +175,6 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
return doc;
}
/**
* Check whether the given item is the latest version.
* If the latest item cannot be determined, because either the version history or the latest version is not present,
* assume the item is latest.
* @param context the DSpace context.
* @param item the item that should be checked.
* @return true if the item is the latest version, false otherwise.
*/
protected boolean isLatestVersion(Context context, Item item) throws SQLException {
VersionHistory history = versionHistoryService.findByItem(context, item);
if (history == null) {
// not all items have a version history
// if an item does not have a version history, it is by definition the latest version
return true;
}
// start with the very latest version of the given item (may still be in workspace)
Version latestVersion = versionHistoryService.getLatestVersion(context, history);
// find the latest version of the given item that is archived
while (latestVersion != null && !latestVersion.getItem().isArchived()) {
latestVersion = versionHistoryService.getPrevious(context, history, latestVersion);
}
// could not find an archived version of the given item
if (latestVersion == null) {
// this scenario should never happen, but let's err on the side of showing too many items vs. to little
// (see discovery.xml, a lot of discovery configs filter out all items that are not the latest version)
return true;
}
// sanity check
assert latestVersion.getItem().isArchived();
return item.equals(latestVersion.getItem());
}
@Override
public SolrInputDocument buildNewDocument(Context context, IndexableItem indexableItem)
throws SQLException, IOException {
@@ -706,7 +667,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl<Indexable
return List.copyOf(workflowItemIndexFactory.getIndexableObjects(context, xmlWorkflowItem));
}
if (!isLatestVersion(context, item)) {
if (!itemService.isLatestVersion(context, item)) {
// the given item is an older version of another item
return List.of(new IndexableItem(item));
}

View File

@@ -14,9 +14,10 @@ import static java.util.Comparator.nullsFirst;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -82,7 +83,7 @@ public class OrcidQueueConsumer implements Consumer {
private RelationshipService relationshipService;
private final List<UUID> alreadyConsumedItems = new ArrayList<>();
private final Set<UUID> itemsToConsume = new HashSet<>();
@Override
public void initialize() throws Exception {
@@ -117,10 +118,16 @@ public class OrcidQueueConsumer implements Consumer {
return;
}
if (alreadyConsumedItems.contains(item.getID())) {
return;
itemsToConsume.add(item.getID());
}
@Override
public void end(Context context) throws Exception {
for (UUID itemId : itemsToConsume) {
Item item = itemService.find(context, itemId);
context.turnOffAuthorisationSystem();
try {
consumeItem(context, item);
@@ -130,6 +137,9 @@ public class OrcidQueueConsumer implements Consumer {
}
itemsToConsume.clear();
}
/**
* Consume the item if it is a profile or an ORCID entity.
*/
@@ -146,7 +156,7 @@ public class OrcidQueueConsumer implements Consumer {
consumeProfile(context, item);
}
alreadyConsumedItems.add(item.getID());
itemsToConsume.add(item.getID());
}
@@ -169,6 +179,10 @@ public class OrcidQueueConsumer implements Consumer {
continue;
}
if (isNotLatestVersion(context, entity)) {
continue;
}
orcidQueueService.create(context, relatedItem, entity);
}
@@ -329,6 +343,14 @@ public class OrcidQueueConsumer implements Consumer {
return !getProfileType().equals(itemService.getEntityTypeLabel(profileItemItem));
}
private boolean isNotLatestVersion(Context context, Item entity) {
try {
return !itemService.isLatestVersion(context, entity);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String getMetadataValue(Item item, String metadataField) {
return itemService.getMetadataFirstValue(item, new MetadataFieldName(metadataField), Item.ANY);
}
@@ -345,11 +367,6 @@ public class OrcidQueueConsumer implements Consumer {
return !configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
@Override
public void end(Context context) throws Exception {
alreadyConsumedItems.clear();
}
@Override
public void finish(Context context) throws Exception {
// nothing to do

View File

@@ -74,6 +74,16 @@ public interface OrcidQueueDAO extends GenericDAO<OrcidQueue> {
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Get the OrcidQueue records where the given item is the entity.
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidQueue entities
* @throws SQLException if database error
*/
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException;
/**
* Find all the OrcidQueue records with the given entity and record type.
*

View File

@@ -63,6 +63,13 @@ public class OrcidQueueDAOImpl extends AbstractHibernateDAO<OrcidQueue> implemen
return query.getResultList();
}
@Override
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE entity.id = :itemId");
query.setParameter("itemId", item.getID());
return query.getResultList();
}
@Override
public List<OrcidQueue> findByEntityAndRecordType(Context context, Item entity, String type) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE entity = :entity AND recordType = :type");

View File

@@ -164,6 +164,16 @@ public interface OrcidQueueService {
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Get the OrcidQueue records where the given item is the entity.
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidQueue records
* @throws SQLException if database error
*/
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException;
/**
* Get all the OrcidQueue records with attempts less than the given attempts.
*

View File

@@ -70,6 +70,11 @@ public class OrcidQueueServiceImpl implements OrcidQueueService {
return orcidQueueDAO.findByProfileItemOrEntity(context, item);
}
@Override
public List<OrcidQueue> findByEntity(Context context, Item item) throws SQLException {
return orcidQueueDAO.findByEntity(context, item);
}
@Override
public long countByProfileItemId(Context context, UUID profileItemId) throws SQLException {
return orcidQueueDAO.countByProfileItemId(context, profileItemId);

View File

@@ -33,6 +33,11 @@ import org.dspace.core.Context;
import org.dspace.discovery.IndexEventConsumer;
import org.dspace.event.Consumer;
import org.dspace.event.Event;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.versioning.factory.VersionServiceFactory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.utils.RelationshipVersioningUtils;
@@ -58,6 +63,8 @@ public class VersioningConsumer implements Consumer {
private RelationshipTypeService relationshipTypeService;
private RelationshipService relationshipService;
private RelationshipVersioningUtils relationshipVersioningUtils;
private OrcidQueueService orcidQueueService;
private OrcidHistoryService orcidHistoryService;
@Override
public void initialize() throws Exception {
@@ -67,6 +74,8 @@ public class VersioningConsumer implements Consumer {
relationshipTypeService = ContentServiceFactory.getInstance().getRelationshipTypeService();
relationshipService = ContentServiceFactory.getInstance().getRelationshipService();
relationshipVersioningUtils = VersionServiceFactory.getInstance().getRelationshipVersioningUtils();
this.orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService();
this.orcidHistoryService = OrcidServiceFactory.getInstance().getOrcidHistoryService();
}
@Override
@@ -132,7 +141,8 @@ public class VersioningConsumer implements Consumer {
// unarchive previous item
unarchiveItem(ctx, previousItem);
// handles versions for ORCID publications waiting to be shipped, or already published (history-queue).
handleOrcidSynchronization(ctx, previousItem, latestItem);
// update relationships
updateRelationships(ctx, latestItem, previousItem);
}
@@ -148,6 +158,29 @@ public class VersioningConsumer implements Consumer {
));
}
private void handleOrcidSynchronization(Context ctx, Item previousItem, Item latestItem) {
try {
replaceOrcidHistoryEntities(ctx, previousItem, latestItem);
removeOrcidQueueEntries(ctx, previousItem);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private void removeOrcidQueueEntries(Context ctx, Item previousItem) throws SQLException {
List<OrcidQueue> queueEntries = orcidQueueService.findByEntity(ctx, previousItem);
for (OrcidQueue queueEntry : queueEntries) {
orcidQueueService.delete(ctx, queueEntry);
}
}
private void replaceOrcidHistoryEntities(Context ctx, Item previousItem, Item latestItem) throws SQLException {
List<OrcidHistory> entries = orcidHistoryService.findByEntity(ctx, previousItem);
for (OrcidHistory entry : entries) {
entry.setEntity(latestItem);
}
}
/**
* Update {@link Relationship#latestVersionStatus} of the relationships of both the old version and the new version
* of the item.

View File

@@ -11,6 +11,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -990,4 +991,38 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
context.restoreAuthSystemState();
}
@Test
public void testIsLatestVersion() throws Exception {
assertTrue("Original should be the latest version", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
Version firstVersion = versioningService.createNewVersion(context, item);
Item firstPublication = firstVersion.getItem();
WorkspaceItem firstPublicationWSI = workspaceItemService.findByItem(context, firstPublication);
installItemService.installItem(context, firstPublicationWSI);
context.commit();
context.restoreAuthSystemState();
assertTrue("First version should be valid", this.itemService.isLatestVersion(context, firstPublication));
assertFalse("Original version should not be valid", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
Version secondVersion = versioningService.createNewVersion(context, item);
Item secondPublication = secondVersion.getItem();
WorkspaceItem secondPublicationWSI = workspaceItemService.findByItem(context, secondPublication);
installItemService.installItem(context, secondPublicationWSI);
context.commit();
context.restoreAuthSystemState();
assertTrue("Second version should be valid", this.itemService.isLatestVersion(context, secondPublication));
assertFalse("First version should not be valid", this.itemService.isLatestVersion(context, firstPublication));
assertFalse("Original version should not be valid", this.itemService.isLatestVersion(context, item));
context.turnOffAuthorisationSystem();
}
}

View File

@@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import java.sql.SQLException;
import java.time.Instant;
@@ -41,13 +42,19 @@ import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.RelationshipType;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.orcid.consumer.OrcidQueueConsumer;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
import org.dspace.versioning.Version;
import org.dspace.versioning.service.VersioningService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -64,8 +71,15 @@ public class OrcidQueueConsumerIT extends AbstractIntegrationTestWithDatabase {
private ItemService itemService = ContentServiceFactory.getInstance().getItemService();
private WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
private InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService();
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
private VersioningService versioningService = new DSpace().getServiceManager()
.getServicesByType(VersioningService.class).get(0);
private Collection profileCollection;
@Before
@@ -763,6 +777,177 @@ public class OrcidQueueConsumerIT extends AbstractIntegrationTestWithDatabase {
}
@Test
public void testOrcidQueueRecordCreationForPublicationWithNotFoundAuthority() throws Exception {
context.turnOffAuthorisationSystem();
Item profile = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withOrcidSynchronizationPublicationsPreference(ALL)
.build();
Collection publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.withAuthor("First User")
.withAuthor("Test User")
.build();
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType,
"isAuthorOfPublication",
"isPublicationOfAuthor", 0, null, 0,
null).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT));
}
@Test
public void testOrcidQueueWithItemVersioning() throws Exception {
context.turnOffAuthorisationSystem();
Item profile = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withOrcidSynchronizationPublicationsPreference(ALL)
.build();
Collection publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.withAuthor("Test User")
.build();
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType,
"isAuthorOfPublication",
"isPublicationOfAuthor", 0, null, 0,
null).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT));
context.turnOffAuthorisationSystem();
Version newVersion = versioningService.createNewVersion(context, publication);
context.restoreAuthSystemState();
Item newPublication = newVersion.getItem();
assertThat(newPublication.isArchived(), is(false));
context.commit();
orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT));
WorkspaceItem workspaceItem = workspaceItemService.findByItem(context, newVersion.getItem());
context.turnOffAuthorisationSystem();
installItemService.installItem(context, workspaceItem);
context.restoreAuthSystemState();
context.commit();
orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, newPublication, "Publication", INSERT));
}
@Test
public void testOrcidQueueUpdateWithItemVersioning() throws Exception {
context.turnOffAuthorisationSystem();
Item profile = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withOrcidSynchronizationPublicationsPreference(ALL)
.build();
Collection publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.build();
OrcidHistory orcidHistory = OrcidHistoryBuilder.createOrcidHistory(context, profile, publication)
.withDescription("Test publication")
.withOperation(OrcidOperation.INSERT)
.withPutCode("12345")
.withStatus(201)
.build();
addMetadata(publication, "dc", "contributor", "author", "Test User", null);
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication =
createRelationshipTypeBuilder(
context, personType, publicationType,
"isAuthorOfPublication",
"isPublicationOfAuthor", 0, null, 0,
null
).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", "12345", UPDATE));
Version newVersion = versioningService.createNewVersion(context, publication);
Item newPublication = newVersion.getItem();
assertThat(newPublication.isArchived(), is(false));
context.commit();
orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", "12345", UPDATE));
WorkspaceItem workspaceItem = workspaceItemService.findByItem(context, newVersion.getItem());
installItemService.installItem(context, workspaceItem);
context.commit();
context.restoreAuthSystemState();
orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, newPublication, "Publication", "12345", UPDATE));
orcidHistory = context.reloadEntity(orcidHistory);
assertThat(orcidHistory.getEntity(), is(newPublication));
}
private void addMetadata(Item item, String schema, String element, String qualifier, String value,
String authority) throws Exception {
context.turnOffAuthorisationSystem();