From 89cb73bfbc6407e3ef70e1a9f6c45376a4fe0c92 Mon Sep 17 00:00:00 2001
From: cryptalith
Date: Wed, 30 Sep 2026 11:51:50 -0600
Subject: [PATCH 1/7] PD-14292 omit write-only contributor email from API work
reads
---
.../impl/MemberV2ApiServiceDelegatorImpl.java | 2 +
.../impl/MemberV3ApiServiceDelegatorImpl.java | 2 +
...MemberV2ApiServiceDelegator_WorksTest.java | 21 +++++++
...berV2ApiServiceVersionedDelegatorTest.java | 21 +++++++
...MemberV3ApiServiceDelegator_WorksTest.java | 21 +++++++
.../orcid/core/utils/ContributorUtils.java | 26 +++++++++
.../orcid/core/utils/v3/ContributorUtils.java | 26 +++++++++
.../core/utils/ContributorUtilsTest.java | 58 +++++++++++++++++++
.../core/utils/v3/ContributorUtilsTest.java | 57 ++++++++++++++++++
.../impl/PublicV2ApiServiceDelegatorImpl.java | 2 +
.../impl/PublicV3ApiServiceDelegatorImpl.java | 2 +
.../PublicV2ApiServiceDelegatorTest.java | 2 +
...licV2ApiServiceVersionedDelegatorTest.java | 20 +++++++
.../PublicV3ApiServiceDelegatorTest.java | 2 +
.../main/resources/data/WorksEntityData.xml | 2 +-
15 files changed, 263 insertions(+), 1 deletion(-)
diff --git a/orcid-api-web/src/main/java/org/orcid/api/memberV2/server/delegator/impl/MemberV2ApiServiceDelegatorImpl.java b/orcid-api-web/src/main/java/org/orcid/api/memberV2/server/delegator/impl/MemberV2ApiServiceDelegatorImpl.java
index d2ba9aba218..154d4a8c344 100644
--- a/orcid-api-web/src/main/java/org/orcid/api/memberV2/server/delegator/impl/MemberV2ApiServiceDelegatorImpl.java
+++ b/orcid-api-web/src/main/java/org/orcid/api/memberV2/server/delegator/impl/MemberV2ApiServiceDelegatorImpl.java
@@ -279,6 +279,7 @@ public Response viewActivities(String orcid) {
public Response viewWork(String orcid, Long putCode) {
Work w = workManagerReadOnly.getWork(orcid, putCode);
orcidSecurityManager.checkAndFilter(orcid, w, ScopePathType.ORCID_WORKS_READ_LIMITED);
+ contributorUtils.filterContributorPrivateData(w);
ActivityUtils.cleanEmptyFields(w);
ActivityUtils.setPathToActivity(w, orcid);
sourceUtils.setSourceName(w);
@@ -1090,6 +1091,7 @@ public Response viewBulkWorks(String orcid, String putCodes) {
WorkBulk workBulk = workManagerReadOnly.findWorkBulk(orcid, putCodes);
orcidSecurityManager.checkAndFilter(orcid, workBulk, ScopePathType.ORCID_WORKS_READ_LIMITED);
+ contributorUtils.filterContributorPrivateData(workBulk);
ActivityUtils.cleanEmptyFields(workBulk);
ActivityUtils.setPathToBulk(workBulk, orcid);
sourceUtils.setSourceName(workBulk);
diff --git a/orcid-api-web/src/main/java/org/orcid/api/memberV3/server/delegator/impl/MemberV3ApiServiceDelegatorImpl.java b/orcid-api-web/src/main/java/org/orcid/api/memberV3/server/delegator/impl/MemberV3ApiServiceDelegatorImpl.java
index b7f727fd197..e8f0b9d89d4 100644
--- a/orcid-api-web/src/main/java/org/orcid/api/memberV3/server/delegator/impl/MemberV3ApiServiceDelegatorImpl.java
+++ b/orcid-api-web/src/main/java/org/orcid/api/memberV3/server/delegator/impl/MemberV3ApiServiceDelegatorImpl.java
@@ -247,6 +247,7 @@ public Response viewWork(String orcid, Long putCode) {
checkProfileStatus(orcid, true);
Work w = workManagerReadOnly.getWork(orcid, putCode);
orcidSecurityManager.checkAndFilter(orcid, w, ScopePathType.ORCID_WORKS_READ_LIMITED);
+ contributorUtils.filterContributorPrivateData(w);
ActivityUtils.cleanEmptyFields(w);
ActivityUtils.setPathToActivity(w, orcid);
sourceUtils.setSourceName(w);
@@ -1114,6 +1115,7 @@ public Response viewBulkWorks(String orcid, String putCodes) {
WorkBulk workBulk = workManagerReadOnly.findWorkBulk(orcid, putCodes);
orcidSecurityManager.checkAndFilter(orcid, workBulk, ScopePathType.ORCID_WORKS_READ_LIMITED);
+ contributorUtils.filterContributorPrivateData(workBulk);
ActivityUtils.cleanEmptyFields(workBulk);
ActivityUtils.setPathToBulk(workBulk, orcid);
sourceUtils.setSourceName(workBulk);
diff --git a/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceDelegator_WorksTest.java b/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceDelegator_WorksTest.java
index ac9c4ad87db..fbe9d43fc23 100644
--- a/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceDelegator_WorksTest.java
+++ b/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceDelegator_WorksTest.java
@@ -174,9 +174,27 @@ public void testViewWorkReadPublic() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
Utils.assertIsPublicOrSource(work, "APP-5555555555555555");
}
+ // Work 11 was deposited by APP-5555555555555555; the email is withheld from every caller
+ @Test
+ public void testViewWorkOmitsContributorEmailForAnotherClientReadPublic() {
+ SecurityContextTestUtils.setUpSecurityContextForClientOnly("APP-6666666666666666", ScopePathType.READ_PUBLIC);
+ Work work = (Work) serviceDelegator.viewWork(ORCID, 11L).getEntity();
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
+ @Test
+ public void testViewWorkOmitsContributorEmailForAnotherClientReadLimited() {
+ SecurityContextTestUtils.setUpSecurityContext(ORCID, "APP-6666666666666666", ScopePathType.READ_LIMITED);
+ Work work = (Work) serviceDelegator.viewWork(ORCID, 11L).getEntity();
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
@Test
public void testViewWorkSummaryReadPublic() {
SecurityContextTestUtils.setUpSecurityContextForClientOnly("APP-5555555555555555", ScopePathType.READ_PUBLIC);
@@ -729,6 +747,9 @@ public void testViewBulkWorks() {
assertEquals("/0000-0000-0000-0003/work/11", ((Work) workBulk.getBulk().get(0)).getPath());
assertEquals("/0000-0000-0000-0003/work/12", ((Work) workBulk.getBulk().get(1)).getPath());
assertEquals("/0000-0000-0000-0003/work/13", ((Work) workBulk.getBulk().get(2)).getPath());
+ Work work11 = (Work) workBulk.getBulk().get(0);
+ assertEquals(1, work11.getWorkContributors().getContributor().size());
+ assertNull(work11.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
diff --git a/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceVersionedDelegatorTest.java b/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceVersionedDelegatorTest.java
index f0e4a850319..b5119067e99 100644
--- a/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceVersionedDelegatorTest.java
+++ b/orcid-api-web/src/test/java/org/orcid/api/memberV2/server/delegator/MemberV2ApiServiceVersionedDelegatorTest.java
@@ -2,6 +2,7 @@
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 static org.junit.Assert.fail;
@@ -1984,6 +1985,26 @@ public void testDeactivatedRecordDeleteAddress() {
fail();
}
+ /**
+ * The contributor email is write-only. The versioned beans run every work through the
+ * 2.0 <-> 2.1 converter, which copies every bean property, so check it stays out on both.
+ */
+ @Test
+ public void testViewWorkOmitsContributorEmailOnV2_0AndV2_1() {
+ SecurityContextTestUtils.setUpSecurityContext("0000-0000-0000-0003", ScopePathType.READ_LIMITED);
+ assertNoContributorEmail((Work) serviceDelegator.viewWork("0000-0000-0000-0003", 11L).getEntity());
+ assertNoContributorEmail((Work) serviceDelegator_v2_1.viewWork("0000-0000-0000-0003", 11L).getEntity());
+ assertNoContributorEmail((Work) ((WorkBulk) serviceDelegator.viewBulkWorks("0000-0000-0000-0003", "11").getEntity()).getBulk().get(0));
+ assertNoContributorEmail((Work) ((WorkBulk) serviceDelegator_v2_1.viewBulkWorks("0000-0000-0000-0003", "11").getEntity()).getBulk().get(0));
+ }
+
+ private void assertNoContributorEmail(Work work) {
+ assertEquals(Long.valueOf(11), work.getPutCode());
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
private void assertSourceElement(SourceAware element, boolean isHttps) {
if(element.getSource() != null && element.getSource().getSourceOrcid() != null) {
assertProtocol(element.getSource().getSourceOrcid().getUri(), isHttps);
diff --git a/orcid-api-web/src/test/java/org/orcid/api/memberV3/server/delegator/MemberV3ApiServiceDelegator_WorksTest.java b/orcid-api-web/src/test/java/org/orcid/api/memberV3/server/delegator/MemberV3ApiServiceDelegator_WorksTest.java
index 12819562718..7da0c8b2ecc 100644
--- a/orcid-api-web/src/test/java/org/orcid/api/memberV3/server/delegator/MemberV3ApiServiceDelegator_WorksTest.java
+++ b/orcid-api-web/src/test/java/org/orcid/api/memberV3/server/delegator/MemberV3ApiServiceDelegator_WorksTest.java
@@ -208,9 +208,27 @@ public void testViewWorkReadPublic() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
Utils.assertIsPublicOrSource(work, "APP-5555555555555555");
}
+ // Work 11 was deposited by APP-5555555555555555; the email is withheld from every caller
+ @Test
+ public void testViewWorkOmitsContributorEmailForAnotherClientReadPublic() {
+ SecurityContextTestUtils.setUpSecurityContextForClientOnly("APP-6666666666666666", ScopePathType.READ_PUBLIC);
+ Work work = (Work) serviceDelegator.viewWork(ORCID, 11L).getEntity();
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
+ @Test
+ public void testViewWorkOmitsContributorEmailForAnotherClientReadLimited() {
+ SecurityContextTestUtils.setUpSecurityContext(ORCID, "APP-6666666666666666", ScopePathType.READ_LIMITED);
+ Work work = (Work) serviceDelegator.viewWork(ORCID, 11L).getEntity();
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
@Test
public void testViewWorkSummaryReadPublic() {
SecurityContextTestUtils.setUpSecurityContextForClientOnly("APP-5555555555555555", ScopePathType.READ_PUBLIC);
@@ -716,6 +734,9 @@ public void testViewBulkWorks() {
assertEquals("/0000-0000-0000-0003/work/11", ((Work) workBulk.getBulk().get(0)).getPath());
assertEquals("/0000-0000-0000-0003/work/12", ((Work) workBulk.getBulk().get(1)).getPath());
assertEquals("/0000-0000-0000-0003/work/13", ((Work) workBulk.getBulk().get(2)).getPath());
+ Work work11 = (Work) workBulk.getBulk().get(0);
+ assertEquals(1, work11.getWorkContributors().getContributor().size());
+ assertNull(work11.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
diff --git a/orcid-core/src/main/java/org/orcid/core/utils/ContributorUtils.java b/orcid-core/src/main/java/org/orcid/core/utils/ContributorUtils.java
index 1741b214190..3eb21d22592 100644
--- a/orcid-core/src/main/java/org/orcid/core/utils/ContributorUtils.java
+++ b/orcid-core/src/main/java/org/orcid/core/utils/ContributorUtils.java
@@ -8,8 +8,11 @@
import org.orcid.jaxb.model.common_v2.Contributor;
import org.orcid.jaxb.model.common_v2.ContributorAttributes;
import org.orcid.jaxb.model.common_v2.CreditName;
+import org.orcid.jaxb.model.record.bulk.BulkElement;
import org.orcid.jaxb.model.record_v2.Funding;
import org.orcid.jaxb.model.record_v2.FundingContributor;
+import org.orcid.jaxb.model.record_v2.Work;
+import org.orcid.jaxb.model.record_v2.WorkBulk;
import org.orcid.persistence.dao.RecordNameDao;
import org.orcid.pojo.ContributorsRolesAndSequencesV2;
import org.orcid.pojo.ajaxForm.PojoUtil;
@@ -55,6 +58,29 @@ public void filterContributorPrivateData(Funding funding) {
}
}
+ /**
+ * The contributor email is write-only (work-3.0.xsd): it is never returned
+ * when reading a work. Unlike the Funding overload, credit names are left
+ * as deposited.
+ */
+ public void filterContributorPrivateData(Work work) {
+ if (work != null && work.getWorkContributors() != null && work.getWorkContributors().getContributor() != null) {
+ for (Contributor contributor : work.getWorkContributors().getContributor()) {
+ contributor.setContributorEmail(null);
+ }
+ }
+ }
+
+ public void filterContributorPrivateData(WorkBulk works) {
+ if (works != null && works.getBulk() != null) {
+ for (BulkElement element : works.getBulk()) {
+ if (element instanceof Work) {
+ filterContributorPrivateData((Work) element);
+ }
+ }
+ }
+ }
+
public List getContributorsGroupedByOrcid(List contributors, Integer maxContributorsForUI) {
List contributorsRolesAndSequencesList = new ArrayList<>();
for(Contributor contributor : contributors) {
diff --git a/orcid-core/src/main/java/org/orcid/core/utils/v3/ContributorUtils.java b/orcid-core/src/main/java/org/orcid/core/utils/v3/ContributorUtils.java
index f6c4248b3cc..98305a5a18d 100644
--- a/orcid-core/src/main/java/org/orcid/core/utils/v3/ContributorUtils.java
+++ b/orcid-core/src/main/java/org/orcid/core/utils/v3/ContributorUtils.java
@@ -4,11 +4,14 @@
import org.orcid.core.contributors.roles.credit.CreditRole;
import org.orcid.core.manager.v3.ProfileEntityManager;
import org.orcid.core.manager.v3.read_only.RecordNameManagerReadOnly;
+import org.orcid.jaxb.model.record.bulk.BulkElement;
import org.orcid.jaxb.model.v3.release.common.Contributor;
import org.orcid.jaxb.model.v3.release.common.ContributorAttributes;
import org.orcid.jaxb.model.v3.release.common.CreditName;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
+import org.orcid.jaxb.model.v3.release.record.Work;
+import org.orcid.jaxb.model.v3.release.record.WorkBulk;
import org.orcid.pojo.ContributorsRolesAndSequences;
import org.orcid.pojo.ajaxForm.PojoUtil;
@@ -43,6 +46,29 @@ public void filterContributorPrivateData(Funding funding) {
}
}
+ /**
+ * The contributor email is write-only (work-3.0.xsd): it is never returned
+ * when reading a work. Unlike the Funding overload, credit names are left
+ * as deposited.
+ */
+ public void filterContributorPrivateData(Work work) {
+ if (work != null && work.getWorkContributors() != null && work.getWorkContributors().getContributor() != null) {
+ for (Contributor contributor : work.getWorkContributors().getContributor()) {
+ contributor.setContributorEmail(null);
+ }
+ }
+ }
+
+ public void filterContributorPrivateData(WorkBulk works) {
+ if (works != null && works.getBulk() != null) {
+ for (BulkElement element : works.getBulk()) {
+ if (element instanceof Work) {
+ filterContributorPrivateData((Work) element);
+ }
+ }
+ }
+ }
+
public List getContributorsGroupedByOrcid(List contributors, Integer maxContributorsForUI) {
List contributorsRolesAndSequencesList = new ArrayList<>();
for(Contributor contributor : contributors) {
diff --git a/orcid-core/src/test/java/org/orcid/core/utils/ContributorUtilsTest.java b/orcid-core/src/test/java/org/orcid/core/utils/ContributorUtilsTest.java
index 39864ed1dde..567eec90471 100644
--- a/orcid-core/src/test/java/org/orcid/core/utils/ContributorUtilsTest.java
+++ b/orcid-core/src/test/java/org/orcid/core/utils/ContributorUtilsTest.java
@@ -25,14 +25,18 @@
import org.orcid.core.manager.ProfileEntityCacheManager;
import org.orcid.core.manager.ProfileEntityManager;
import org.orcid.jaxb.model.common_v2.Contributor;
+import org.orcid.jaxb.model.common_v2.ContributorAttributes;
import org.orcid.jaxb.model.common_v2.ContributorEmail;
import org.orcid.jaxb.model.common_v2.ContributorOrcid;
+import org.orcid.jaxb.model.common_v2.ContributorRole;
import org.orcid.jaxb.model.common_v2.CreditName;
import org.orcid.jaxb.model.common_v2.Title;
+import org.orcid.jaxb.model.error_v2.OrcidError;
import org.orcid.jaxb.model.record_v2.Funding;
import org.orcid.jaxb.model.record_v2.FundingContributor;
import org.orcid.jaxb.model.record_v2.FundingContributors;
import org.orcid.jaxb.model.record_v2.FundingTitle;
+import org.orcid.jaxb.model.record_v2.SequenceType;
import org.orcid.jaxb.model.record_v2.Work;
import org.orcid.jaxb.model.record_v2.WorkBulk;
import org.orcid.jaxb.model.record_v2.WorkContributors;
@@ -131,6 +135,60 @@ public void testFilterContributorPrivateDataForFundingWithoutContributors() {
assertNotNull(funding); // test no failures
}
+ @Test
+ public void testFilterContributorPrivateDataForWork() {
+ // A public name is available, so a name replacement would be visible
+ when(profileEntityManager.orcidExists(anyString())).thenReturn(true);
+ when(cacheManager.getPublicCreditName(any(String.class))).thenReturn("a public name");
+
+ Work work = getWorkWithOrcidContributor();
+ ContributorAttributes attributes = new ContributorAttributes();
+ attributes.setContributorRole(ContributorRole.AUTHOR);
+ attributes.setContributorSequence(SequenceType.FIRST);
+ work.getWorkContributors().getContributor().get(0).setContributorAttributes(attributes);
+
+ contributorUtils.filterContributorPrivateData(work);
+
+ Contributor contributor = work.getWorkContributors().getContributor().get(0);
+ assertNull(contributor.getContributorEmail());
+ assertEquals("original credit name", contributor.getCreditName().getContent());
+ assertEquals("0000-0003-4902-6327", contributor.getContributorOrcid().getPath());
+ assertEquals(ContributorRole.AUTHOR, contributor.getContributorAttributes().getContributorRole());
+ assertEquals(SequenceType.FIRST, contributor.getContributorAttributes().getContributorSequence());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForWorkWithNoOrcidRecord() {
+ Work work = getWorkWithContributorWithoutOrcid();
+ contributorUtils.filterContributorPrivateData(work);
+
+ Contributor contributor = work.getWorkContributors().getContributor().get(0);
+ assertNull(contributor.getContributorEmail());
+ assertEquals("original credit name", contributor.getCreditName().getContent());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForWorkWithoutContributors() {
+ Work work = getWorkWithoutContributors();
+ contributorUtils.filterContributorPrivateData(work);
+ assertNull(work.getWorkContributors());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForBulkWork() {
+ OrcidError error = new OrcidError();
+ error.setDeveloperMessage("not a work");
+ WorkBulk bulk = new WorkBulk();
+ bulk.setBulk(new ArrayList<>(Arrays.asList(getWorkWithOrcidContributor(), error, getWorkWithContributorWithoutOrcid())));
+
+ contributorUtils.filterContributorPrivateData(bulk);
+
+ assertEquals(3, bulk.getBulk().size());
+ assertNull(((Work) bulk.getBulk().get(0)).getWorkContributors().getContributor().get(0).getContributorEmail());
+ assertEquals("not a work", ((OrcidError) bulk.getBulk().get(1)).getDeveloperMessage());
+ assertNull(((Work) bulk.getBulk().get(2)).getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
private Work getWorkWithoutContributors() {
Work work = new Work();
WorkTitle workTitle = new WorkTitle();
diff --git a/orcid-core/src/test/java/org/orcid/core/utils/v3/ContributorUtilsTest.java b/orcid-core/src/test/java/org/orcid/core/utils/v3/ContributorUtilsTest.java
index 00dcaeb5445..38597193ef7 100644
--- a/orcid-core/src/test/java/org/orcid/core/utils/v3/ContributorUtilsTest.java
+++ b/orcid-core/src/test/java/org/orcid/core/utils/v3/ContributorUtilsTest.java
@@ -25,11 +25,14 @@
import org.orcid.core.manager.v3.ActivityManager;
import org.orcid.core.manager.v3.ProfileEntityManager;
import org.orcid.core.manager.v3.read_only.RecordNameManagerReadOnly;
+import org.orcid.jaxb.model.common.SequenceType;
import org.orcid.jaxb.model.v3.release.common.Contributor;
+import org.orcid.jaxb.model.v3.release.common.ContributorAttributes;
import org.orcid.jaxb.model.v3.release.common.ContributorEmail;
import org.orcid.jaxb.model.v3.release.common.ContributorOrcid;
import org.orcid.jaxb.model.v3.release.common.CreditName;
import org.orcid.jaxb.model.v3.release.common.Title;
+import org.orcid.jaxb.model.v3.release.error.OrcidError;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
import org.orcid.jaxb.model.v3.release.record.FundingContributors;
@@ -130,6 +133,60 @@ public void testFilterContributorPrivateDataForFundingWithoutContributors() {
assertNotNull(funding); // test no failures
}
+ @Test
+ public void testFilterContributorPrivateDataForWork() {
+ // A public name is available, so a name replacement would be visible
+ when(profileEntityManager.orcidExists(anyString())).thenReturn(true);
+ when(recordNameManagerReadOnlyV3.fetchDisplayablePublicName(anyString())).thenReturn("a public name");
+
+ Work work = getWorkWithOrcidContributor();
+ ContributorAttributes attributes = new ContributorAttributes();
+ attributes.setContributorRole("author");
+ attributes.setContributorSequence(SequenceType.FIRST);
+ work.getWorkContributors().getContributor().get(0).setContributorAttributes(attributes);
+
+ contributorUtils.filterContributorPrivateData(work);
+
+ Contributor contributor = work.getWorkContributors().getContributor().get(0);
+ assertNull(contributor.getContributorEmail());
+ assertEquals("original credit name", contributor.getCreditName().getContent());
+ assertEquals("0000-0003-4902-6327", contributor.getContributorOrcid().getPath());
+ assertEquals("author", contributor.getContributorAttributes().getContributorRole());
+ assertEquals(SequenceType.FIRST, contributor.getContributorAttributes().getContributorSequence());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForWorkWithNoOrcidRecord() {
+ Work work = getWorkWithContributorWithoutOrcid();
+ contributorUtils.filterContributorPrivateData(work);
+
+ Contributor contributor = work.getWorkContributors().getContributor().get(0);
+ assertNull(contributor.getContributorEmail());
+ assertEquals("original credit name", contributor.getCreditName().getContent());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForWorkWithoutContributors() {
+ Work work = getWorkWithoutContributors();
+ contributorUtils.filterContributorPrivateData(work);
+ assertNull(work.getWorkContributors());
+ }
+
+ @Test
+ public void testFilterContributorPrivateDataForBulkWork() {
+ OrcidError error = new OrcidError();
+ error.setDeveloperMessage("not a work");
+ WorkBulk bulk = new WorkBulk();
+ bulk.setBulk(new ArrayList<>(Arrays.asList(getWorkWithOrcidContributor(), error, getWorkWithContributorWithoutOrcid())));
+
+ contributorUtils.filterContributorPrivateData(bulk);
+
+ assertEquals(3, bulk.getBulk().size());
+ assertNull(((Work) bulk.getBulk().get(0)).getWorkContributors().getContributor().get(0).getContributorEmail());
+ assertEquals("not a work", ((OrcidError) bulk.getBulk().get(1)).getDeveloperMessage());
+ assertNull(((Work) bulk.getBulk().get(2)).getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
private Work getWorkWithoutContributors() {
Work work = new Work();
WorkTitle workTitle = new WorkTitle();
diff --git a/orcid-pub-web/src/main/java/org/orcid/api/publicV2/server/delegator/impl/PublicV2ApiServiceDelegatorImpl.java b/orcid-pub-web/src/main/java/org/orcid/api/publicV2/server/delegator/impl/PublicV2ApiServiceDelegatorImpl.java
index ae7c00a46ff..4b45bce7359 100644
--- a/orcid-pub-web/src/main/java/org/orcid/api/publicV2/server/delegator/impl/PublicV2ApiServiceDelegatorImpl.java
+++ b/orcid-pub-web/src/main/java/org/orcid/api/publicV2/server/delegator/impl/PublicV2ApiServiceDelegatorImpl.java
@@ -207,6 +207,7 @@ public Response viewActivities(String orcid) {
public Response viewWork(String orcid, Long putCode) {
Work w = workManagerReadOnly.getWork(orcid, putCode);
publicAPISecurityManagerV2.checkIsPublic(w);
+ contributorUtilsReadOnly.filterContributorPrivateData(w);
ActivityUtils.cleanEmptyFields(w);
ActivityUtils.setPathToActivity(w, orcid);
@@ -557,6 +558,7 @@ public Response viewBulkWorks(String orcid, String putCodes) {
}
WorkBulk workBulk = workManagerReadOnly.findWorkBulk(orcid, putCodes);
publicAPISecurityManagerV2.filter(workBulk);
+ contributorUtilsReadOnly.filterContributorPrivateData(workBulk);
ActivityUtils.cleanEmptyFields(workBulk);
ActivityUtils.setPathToBulk(workBulk, orcid);
sourceUtils.setSourceName(workBulk);
diff --git a/orcid-pub-web/src/main/java/org/orcid/api/publicV3/server/delegator/impl/PublicV3ApiServiceDelegatorImpl.java b/orcid-pub-web/src/main/java/org/orcid/api/publicV3/server/delegator/impl/PublicV3ApiServiceDelegatorImpl.java
index 124ecbd44aa..722be3d0cfc 100644
--- a/orcid-pub-web/src/main/java/org/orcid/api/publicV3/server/delegator/impl/PublicV3ApiServiceDelegatorImpl.java
+++ b/orcid-pub-web/src/main/java/org/orcid/api/publicV3/server/delegator/impl/PublicV3ApiServiceDelegatorImpl.java
@@ -246,6 +246,7 @@ public Response viewWork(String orcid, Long putCode) {
checkProfileStatus(orcid);
Work w = workManagerReadOnly.getWork(orcid, putCode);
publicAPISecurityManagerV3.checkIsPublic(w);
+ contributorUtilsReadOnly.filterContributorPrivateData(w);
ActivityUtils.cleanEmptyFields(w);
ActivityUtils.setPathToActivity(w, orcid);
sourceUtilsReadOnly.setSourceName(w);
@@ -645,6 +646,7 @@ public Response viewBulkWorks(String orcid, String putCodes) {
}
WorkBulk workBulk = workManagerReadOnly.findWorkBulk(orcid, putCodes);
publicAPISecurityManagerV3.filter(workBulk);
+ contributorUtilsReadOnly.filterContributorPrivateData(workBulk);
ActivityUtils.cleanEmptyFields(workBulk);
ActivityUtils.setPathToBulk(workBulk, orcid);
sourceUtils.setSourceName(workBulk);
diff --git a/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceDelegatorTest.java b/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceDelegatorTest.java
index e5a73d9f004..e34f4d971d0 100644
--- a/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceDelegatorTest.java
+++ b/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceDelegatorTest.java
@@ -155,6 +155,7 @@ public void testViewWork() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
@@ -205,6 +206,7 @@ public void testViewBulkWorks() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
diff --git a/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceVersionedDelegatorTest.java b/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceVersionedDelegatorTest.java
index 5dceaabc87c..d32679e6c56 100644
--- a/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceVersionedDelegatorTest.java
+++ b/orcid-pub-web/src/test/java/org/orcid/api/publicV2/server/PublicV2ApiServiceVersionedDelegatorTest.java
@@ -2,6 +2,7 @@
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 static org.junit.Assert.fail;
@@ -990,6 +991,25 @@ public void testViewPersonAndRecordKeepVerifiedAndPrimaryOnV2_1() {
assertEmailFlags(record.getPerson().getEmails().getEmails());
}
+ /**
+ * The contributor email is write-only. The versioned beans run every work through the
+ * 2.0 <-> 2.1 converter, which copies every bean property, so check it stays out on both.
+ */
+ @Test
+ public void testViewWorkOmitsContributorEmailOnV2_0AndV2_1() {
+ assertNoContributorEmail((Work) serviceDelegator.viewWork(userWithPublicEmails, 11L).getEntity());
+ assertNoContributorEmail((Work) serviceDelegatorV2_1.viewWork(userWithPublicEmails, 11L).getEntity());
+ assertNoContributorEmail((Work) ((WorkBulk) serviceDelegator.viewBulkWorks(userWithPublicEmails, "11").getEntity()).getBulk().get(0));
+ assertNoContributorEmail((Work) ((WorkBulk) serviceDelegatorV2_1.viewBulkWorks(userWithPublicEmails, "11").getEntity()).getBulk().get(0));
+ }
+
+ private void assertNoContributorEmail(Work work) {
+ assertEquals(Long.valueOf(11), work.getPutCode());
+ assertEquals(1, work.getWorkContributors().getContributor().size());
+ assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
+ }
+
/**
* Compares values rather than asserting true: a dropped flag is null, and false has to stay
* false rather than collapse to null.
diff --git a/orcid-pub-web/src/test/java/org/orcid/api/publicV3/server/PublicV3ApiServiceDelegatorTest.java b/orcid-pub-web/src/test/java/org/orcid/api/publicV3/server/PublicV3ApiServiceDelegatorTest.java
index f1bd4e59461..9757db70723 100644
--- a/orcid-pub-web/src/test/java/org/orcid/api/publicV3/server/PublicV3ApiServiceDelegatorTest.java
+++ b/orcid-pub-web/src/test/java/org/orcid/api/publicV3/server/PublicV3ApiServiceDelegatorTest.java
@@ -183,6 +183,7 @@ public void testViewWork() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
@@ -233,6 +234,7 @@ public void testViewBulkWorks() {
assertNotNull(work.getWorkContributors().getContributor().get(0).getContributorOrcid());
assertEquals("0000-0000-0000-0000", work.getWorkContributors().getContributor().get(0).getContributorOrcid().getPath());
assertNull(work.getWorkContributors().getContributor().get(0).getCreditName());
+ assertNull(work.getWorkContributors().getContributor().get(0).getContributorEmail());
}
@Test
diff --git a/orcid-test/src/main/resources/data/WorksEntityData.xml b/orcid-test/src/main/resources/data/WorksEntityData.xml
index 56d886530fa..97e383b79b0 100644
--- a/orcid-test/src/main/resources/data/WorksEntityData.xml
+++ b/orcid-test/src/main/resources/data/WorksEntityData.xml
@@ -180,7 +180,7 @@
added_to_profile_date="2016-04-01 11:09:28.636"
visibility="PUBLIC"
external_ids_json="{"workExternalIdentifier":[{"workExternalIdentifierType":"DOI","relationship":"self","workExternalIdentifierId":{"content":"1"}}],"scope":null}"
- contributors_json="{"contributor":[{"contributorOrcid":{"path":"0000-0000-0000-0000"},"creditName":{"content":null},"contributorEmail":null,"contributorAttributes":null}]}"
+ contributors_json="{"contributor":[{"contributorOrcid":{"path":"0000-0000-0000-0000"},"creditName":{"content":null},"contributorEmail":{"value":"never@show.this"},"contributorAttributes":null}]}"
display_index="0"
work_url="http://testuri.org"
featured_display_index="0"
From 41d17c2da7015aa387513865520a57966ed92fa9 Mon Sep 17 00:00:00 2001
From: amontenegro
Date: Wed, 7 Oct 2026 15:04:44 -0600
Subject: [PATCH 2/7] Allow any know class to be deserialized
---
.../OrcidBeanClassLoaderAware.java | 11 +++++++++++
...rcidRedisIndexedSessionRepositoryTest.java | 19 +++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/orcid-web/src/main/java/org/orcid/frontend/spring/configuration/OrcidBeanClassLoaderAware.java b/orcid-web/src/main/java/org/orcid/frontend/spring/configuration/OrcidBeanClassLoaderAware.java
index 262b5e1b70f..57547d94e77 100644
--- a/orcid-web/src/main/java/org/orcid/frontend/spring/configuration/OrcidBeanClassLoaderAware.java
+++ b/orcid-web/src/main/java/org/orcid/frontend/spring/configuration/OrcidBeanClassLoaderAware.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import org.orcid.authorization.authentication.MFAWebAuthenticationDetails;
import org.orcid.frontend.web.util.MFAWebAuthenticationDetailsDeserializer;
import org.orcid.frontend.web.util.SwitchUserGrantedAuthorityDeserializer;
@@ -34,6 +35,16 @@ private ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
mapper.registerModules(new CoreJackson2Module());
+ mapper.activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder()
+ .allowIfSubType("java.lang")
+ .allowIfSubType("org.orcid")
+ .allowIfSubType("org.springframework.security")
+ .allowIfSubType("jakarta.servlet")
+ .allowIfSubType("java.util")
+ .build(),
+ ObjectMapper.DefaultTyping.NON_FINAL,
+ JsonTypeInfo.As.PROPERTY);
mapper.addMixIn(String[].class, StringArrayMixin.class);
mapper.addMixIn(SwitchUserGrantedAuthority.class, SwitchUserGrantedAuthorityMixin.class);
mapper.addMixIn(MFAWebAuthenticationDetails.class, MFAWebAuthenticationDetailsMixin.class);
diff --git a/orcid-web/src/test/java/org/orcid/frontend/spring/session/redis/OrcidRedisIndexedSessionRepositoryTest.java b/orcid-web/src/test/java/org/orcid/frontend/spring/session/redis/OrcidRedisIndexedSessionRepositoryTest.java
index c3084ba9806..2f1640593aa 100644
--- a/orcid-web/src/test/java/org/orcid/frontend/spring/session/redis/OrcidRedisIndexedSessionRepositoryTest.java
+++ b/orcid-web/src/test/java/org/orcid/frontend/spring/session/redis/OrcidRedisIndexedSessionRepositoryTest.java
@@ -17,12 +17,16 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import org.orcid.frontend.spring.configuration.OrcidBeanClassLoaderAware;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.web.context.request.RequestContextHolder;
+import static org.junit.Assert.assertEquals;
+
@RunWith(MockitoJUnitRunner.class)
public class OrcidRedisIndexedSessionRepositoryTest {
@@ -83,4 +87,19 @@ public void deleteByIdShouldWorkWithoutHttpRequestContext() {
verify(principalSetOperations).remove(eq(SESSION_ID));
verify(expirationSetOperations).remove(eq("expires:" + SESSION_ID));
}
+
+ @Test
+ public void springSessionSerializerShouldAllowJavaLongsForSessionDelta() {
+ OrcidBeanClassLoaderAware beanClassLoaderAware = new OrcidBeanClassLoaderAware();
+ beanClassLoaderAware.setBeanClassLoader(getClass().getClassLoader());
+
+ RedisSerializer
<#elseif notificationType == 'AMENDED' && !verboseNotifications>
diff --git a/orcid-core/src/main/resources/org/orcid/core/template/digest_notification.ftl b/orcid-core/src/main/resources/org/orcid/core/template/digest_notification.ftl
index 20a0a05974c..6c2ceddd54a 100644
--- a/orcid-core/src/main/resources/org/orcid/core/template/digest_notification.ftl
+++ b/orcid-core/src/main/resources/org/orcid/core/template/digest_notification.ftl
@@ -39,6 +39,7 @@ ${'\n'}${emailName}<@emailMacros.space /><@emailMacros.msg "notification.digest.
<#else>
<#if notification.notificationIntro??>${notification.notificationIntro}#if>
${(digestEmail.notificationsBySourceId[sourceId].source.sourceName.content)!sourceId}: <#if notification.notificationSubject??>${notification.notificationSubject} <#if notification.createdDate??>(${notification.createdDate.year?c}-<#if notification.createdDate.month?string?length == 1>0${notification.createdDate.month?c}<#else>${notification.createdDate.month?c}#if>-<#if notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}#if>)#if><#else><@emailMacros.msg "email.digest.requesttoadd" /> <#if notification.createdDate??>(${notification.createdDate.year?c}-<#if notification.createdDate.month?string?length == 1>0${notification.createdDate.month?c}<#else>${notification.createdDate.month?c}#if>-<#if notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}#if>)#if>#if>
+ <#if notification.items?? && notification.items.itemsByType?has_content>
<#assign itemsByType=notification.items.itemsByType>
<#list itemsByType?keys?sort as itemType>
<@emailMacros.msg "email.common.recordsection." + itemType /> (${itemsByType[itemType]?size})
@@ -49,6 +50,10 @@ ${'\n'}${emailName}<@emailMacros.space /><@emailMacros.msg "notification.digest.
<@emailMacros.msg "email.digest.plaintext.addnow" /><@emailMacros.space />${baseUri}/inbox/encrypted/${notification.encryptedPutCode}/action
<@emailMacros.msg "email.digest.plaintext.moreinfo" /><@emailMacros.space />${baseUri}/inbox#${notification.putCode}
#list>
+ <#else>
+ <@emailMacros.msg "email.digest.plaintext.addnow" /><@emailMacros.space />${baseUri}/inbox/encrypted/${notification.encryptedPutCode}/action
+ <@emailMacros.msg "email.digest.plaintext.moreinfo" /><@emailMacros.space />${baseUri}/inbox#${notification.putCode}
+ #if>
#if>
<#elseif notificationType == 'AMENDED' && !verboseNotifications>
<#assign amendedSection><@emailMacros.msg "email.common.recordsection." + notification.amendedSection />#assign>
diff --git a/orcid-core/src/main/resources/org/orcid/core/template/digest_notification_html.ftl b/orcid-core/src/main/resources/org/orcid/core/template/digest_notification_html.ftl
index 68b792b10d8..26e7d1f6276 100644
--- a/orcid-core/src/main/resources/org/orcid/core/template/digest_notification_html.ftl
+++ b/orcid-core/src/main/resources/org/orcid/core/template/digest_notification_html.ftl
@@ -110,6 +110,7 @@
<#else>
<#if notification.notificationIntro??>
${notification.notificationIntro}
#if>
<#if notification.notificationSubject??>${notification.notificationSubject} <#if notification.createdDate??>(${notification.createdDate.year?c}-<#if notification.createdDate.month?string?length == 1>0${notification.createdDate.month?c}<#else>${notification.createdDate.month?c}#if>-<#if notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}#if>)#if><#else><@emailMacros.msg "email.digest.requesttoadd" /> <#if notification.createdDate??>(${notification.createdDate.year?c}-<#if notification.createdDate.month?string?length == 1>0${notification.createdDate.month?c}<#else>${notification.createdDate.month?c}#if>-<#if notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}#if>)#if>#if>
+ <#if notification.items??>
<#assign itemsByType=notification.items.itemsByType>
<#list itemsByType?keys?sort as itemType>
@@ -125,6 +126,7 @@
#list>
+ #if>
ItemType.WORK.equals(i.getItemType()))
+ .findFirst().orElse(null);
+ assertNotNull(item1);
+ assertEquals("101", item1.getPutCode());
+ assertEquals("Work Title", item1.getItemName());
+ assertNotNull(item1.getExternalIdentifier());
+ assertEquals("doi", item1.getExternalIdentifier().getType());
+ assertEquals("10.1234/test", item1.getExternalIdentifier().getValue());
+ assertEquals("https://doi.org/10.1234/test", item1.getExternalIdentifier().getUrl().getValue());
+ assertEquals(org.orcid.jaxb.model.record_v2.Relationship.SELF, item1.getExternalIdentifier().getRelationship());
+
+ Item item2 = permissionNotification.getItems().getItems().stream()
+ .filter(i -> ItemType.EMPLOYMENT.equals(i.getItemType()))
+ .findFirst().orElse(null);
+ assertNotNull(item2);
+ assertEquals("102", item2.getPutCode());
+ assertEquals("University of Testing", item2.getItemName());
+ assertNull(item2.getExternalIdentifier());
+ }
+
@Test
public void testToNotificationPermissionEntity() {
NotificationPermission notification = new NotificationPermission();
diff --git a/orcid-core/src/test/java/org/orcid/core/adapter/v3/JpaJaxbNotificationAdapterTest.java b/orcid-core/src/test/java/org/orcid/core/adapter/v3/JpaJaxbNotificationAdapterTest.java
index 7b7335a99df..c871d2fb326 100644
--- a/orcid-core/src/test/java/org/orcid/core/adapter/v3/JpaJaxbNotificationAdapterTest.java
+++ b/orcid-core/src/test/java/org/orcid/core/adapter/v3/JpaJaxbNotificationAdapterTest.java
@@ -155,6 +155,7 @@ public void testEntityToNotificationWithSource() {
Notification amendedNotification = jpaJaxbNotificationAdapter.toNotification(amendedEntity);
assertNotNull(amendedNotification);
assertTrue(amendedNotification instanceof NotificationAmended);
+ assertNotNull(((NotificationAmended) amendedNotification).getItems());
assertNotNull(amendedNotification.getSource());
assertNotNull(amendedNotification.getSource().getSourceClientId());
assertEquals(CLIENT_SOURCE_ID, amendedNotification.getSource().getSourceClientId().getPath());
@@ -169,6 +170,7 @@ public void testEntityToNotificationWithSource() {
Notification permissionNotification = jpaJaxbNotificationAdapter.toNotification(addItemsEntity);
assertNotNull(permissionNotification);
assertTrue(permissionNotification instanceof NotificationPermission);
+ assertNotNull(((NotificationPermission) permissionNotification).getItems());
assertNotNull(permissionNotification.getSource());
assertNotNull(permissionNotification.getSource().getSourceClientId());
assertEquals(CLIENT_SOURCE_ID, permissionNotification.getSource().getSourceClientId().getPath());
@@ -176,6 +178,60 @@ public void testEntityToNotificationWithSource() {
assertEquals("Client name", permissionNotification.getSource().getSourceName().getContent());
}
+ @Test
+ public void testToNotificationPermissionWithItems() {
+ NotificationAddItemsEntity addItemsEntity = new NotificationAddItemsEntity();
+ addItemsEntity.setId(125L);
+ addItemsEntity.setNotificationType(org.orcid.jaxb.model.notification_v2.NotificationType.PERMISSION.name());
+ addItemsEntity.setClientSourceId(CLIENT_SOURCE_ID);
+ addItemsEntity.setAuthorizationUrl("https://orcid.org/oauth/authorize?client_id=APP-1234");
+
+ NotificationItemEntity itemEntity1 = new NotificationItemEntity();
+ itemEntity1.setId(101L);
+ itemEntity1.setItemType("WORK");
+ itemEntity1.setItemName("Work Title");
+ itemEntity1.setExternalIdType("DOI");
+ itemEntity1.setExternalIdValue("10.1234/test");
+ itemEntity1.setExternalIdUrl("https://doi.org/10.1234/test");
+ itemEntity1.setExternalIdRelationship("SELF");
+
+ NotificationItemEntity itemEntity2 = new NotificationItemEntity();
+ itemEntity2.setId(102L);
+ itemEntity2.setItemType("EMPLOYMENT");
+ itemEntity2.setItemName("University of Testing");
+
+ addItemsEntity.setNotificationItems(java.util.Set.of(itemEntity1, itemEntity2));
+
+ Notification notification = jpaJaxbNotificationAdapter.toNotification(addItemsEntity);
+ assertNotNull(notification);
+ assertTrue(notification instanceof NotificationPermission);
+ NotificationPermission permissionNotification = (NotificationPermission) notification;
+ assertNotNull(permissionNotification.getItems());
+ assertNotNull(permissionNotification.getItems().getItems());
+ assertEquals(2, permissionNotification.getItems().getItems().size());
+ assertEquals(2, permissionNotification.getItems().getItemsByType().size());
+
+ Item item1 = permissionNotification.getItems().getItems().stream()
+ .filter(i -> ItemType.WORK.equals(i.getItemType()))
+ .findFirst().orElse(null);
+ assertNotNull(item1);
+ assertEquals("101", item1.getPutCode());
+ assertEquals("Work Title", item1.getItemName());
+ assertNotNull(item1.getExternalIdentifier());
+ assertEquals("doi", item1.getExternalIdentifier().getType());
+ assertEquals("10.1234/test", item1.getExternalIdentifier().getValue());
+ assertEquals("https://doi.org/10.1234/test", item1.getExternalIdentifier().getUrl().getValue());
+ assertEquals(Relationship.SELF, item1.getExternalIdentifier().getRelationship());
+
+ Item item2 = permissionNotification.getItems().getItems().stream()
+ .filter(i -> ItemType.EMPLOYMENT.equals(i.getItemType()))
+ .findFirst().orElse(null);
+ assertNotNull(item2);
+ assertEquals("102", item2.getPutCode());
+ assertEquals("University of Testing", item2.getItemName());
+ assertNull(item2.getExternalIdentifier());
+ }
+
@Test
public void testToNotificationPermissionEntity() {
NotificationPermission notification = new NotificationPermission();
diff --git a/orcid-scheduler-web/src/test/java/org/orcid/scheduler/email/cli/manager/EmailMessageSenderTest.java b/orcid-scheduler-web/src/test/java/org/orcid/scheduler/email/cli/manager/EmailMessageSenderTest.java
index 5056cdb734d..533e4155c4a 100644
--- a/orcid-scheduler-web/src/test/java/org/orcid/scheduler/email/cli/manager/EmailMessageSenderTest.java
+++ b/orcid-scheduler-web/src/test/java/org/orcid/scheduler/email/cli/manager/EmailMessageSenderTest.java
@@ -187,6 +187,32 @@ public void digestDoesNotEvaluateStoredNotificationBody() {
assertTrue("stored markup should still render", html.contains("Hello from "));
}
+ @Test
+ public void testPermissionNotificationEmail() {
+ NotificationPermission notification = new NotificationPermission();
+ notification.setPutCode(100L);
+ notification.setNotificationType(NotificationType.PERMISSION);
+ notification.setSubject("Connect with your organization");
+ notification.setNotificationSubject("Connect with your organization");
+ notification.setNotificationIntro("University of Testing");
+ notification.setAuthorizationUrl(new AuthorizationUrl("https://orcid.org/oauth/authorize?client_id=APP-1234"));
+ notification.setCreatedDate(DateUtils.convertToXMLGregorianCalendar("2026-10-01T12:00:00"));
+ Source source = new Source();
+ source.setSourceName(new SourceName("University of Testing"));
+ source.setSourceClientId(new SourceClientId("APP-1234567890123456"));
+ notification.setSource(source);
+
+ List notifications = new ArrayList<>();
+ notifications.add(notification);
+
+ EmailMessage emailMessage = emailMessageSender.createDigest("0000-0000-0000-0000", notifications);
+ assertNotNull(emailMessage);
+ assertNotNull(emailMessage.getBodyHtml());
+ assertNotNull(emailMessage.getBodyText());
+ assertTrue(emailMessage.getBodyHtml().contains("University of Testing"));
+ assertTrue(emailMessage.getBodyText().contains("University of Testing"));
+ }
+
private List generateNotifications() {
List notifications = new ArrayList<>();
From 59ce342933e1681882db75c3735046ec29d6e158 Mon Sep 17 00:00:00 2001
From: camdum <62257307+Camelia-Orcid@users.noreply.github.com>
Date: Thu, 8 Oct 2026 19:28:28 +0100
Subject: [PATCH 4/7] Resolved EntityExistsException when saving activities and
creating notification items by ignoring id when mapping Item to
NotificationItemEntity
---
.../org/orcid/core/adapter/mapstruct/NotificationMapperV2.java | 2 +-
.../org/orcid/core/adapter/mapstruct/NotificationMapperV3.java | 2 +-
.../orcid/core/adapter/mapstruct/NotificationMapperV2Test.java | 3 ++-
.../orcid/core/adapter/mapstruct/NotificationMapperV3Test.java | 3 ++-
4 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2.java b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2.java
index e5f5611032f..000e570c0ac 100644
--- a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2.java
+++ b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2.java
@@ -29,7 +29,7 @@ public interface NotificationMapperV2 {
NotificationMapperV2 INSTANCE = Mappers.getMapper(NotificationMapperV2.class);
- @Mapping(source = "putCode", target = "id")
+ @Mapping(target = "id", ignore = true)
@Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb")
@Mapping(source = "externalIdentifier.value", target = "externalIdValue")
@Mapping(source = "externalIdentifier.url", target = "externalIdUrl")
diff --git a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3.java b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3.java
index ad2ddc347f3..6951f11afe8 100644
--- a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3.java
+++ b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3.java
@@ -30,7 +30,7 @@ public interface NotificationMapperV3 {
NotificationMapperV3 INSTANCE = Mappers.getMapper(NotificationMapperV3.class);
- @Mapping(source = "putCode", target = "id")
+ @Mapping(target = "id", ignore = true)
@Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb")
@Mapping(source = "externalIdentifier.value", target = "externalIdValue")
@Mapping(source = "externalIdentifier.url", target = "externalIdUrl")
diff --git a/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2Test.java b/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2Test.java
index ed71e93e65b..489a0d7dc7a 100644
--- a/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2Test.java
+++ b/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV2Test.java
@@ -118,7 +118,7 @@ public void testToItemAndToNotificationItemEntity() {
NotificationItemEntity entity = mapper.toNotificationItemEntity(item);
assertNotNull(entity);
- assertEquals(Long.valueOf(123L), entity.getId());
+ org.junit.Assert.assertNull(entity.getId());
assertEquals("WORK", entity.getItemType());
assertEquals("Work Name", entity.getItemName());
assertEquals("DOI", entity.getExternalIdType());
@@ -126,6 +126,7 @@ public void testToItemAndToNotificationItemEntity() {
assertEquals("https://doi.org/10.1234/test", entity.getExternalIdUrl());
assertEquals("SELF", entity.getExternalIdRelationship());
+ entity.setId(123L);
Item mappedBack = mapper.toItem(entity);
assertNotNull(mappedBack);
assertEquals("123", mappedBack.getPutCode());
diff --git a/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3Test.java b/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3Test.java
index 5f06fb78bfc..4e54f30cb1b 100644
--- a/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3Test.java
+++ b/orcid-core/src/test/java/org/orcid/core/adapter/mapstruct/NotificationMapperV3Test.java
@@ -115,7 +115,7 @@ public void testToItemAndToNotificationItemEntity() {
NotificationItemEntity entity = mapper.toNotificationItemEntity(item);
assertNotNull(entity);
- assertEquals(Long.valueOf(123L), entity.getId());
+ org.junit.Assert.assertNull(entity.getId());
assertEquals("WORK", entity.getItemType());
assertEquals("Work Name", entity.getItemName());
assertEquals("DOI", entity.getExternalIdType());
@@ -123,6 +123,7 @@ public void testToItemAndToNotificationItemEntity() {
assertEquals("https://doi.org/10.1234/test", entity.getExternalIdUrl());
assertEquals("SELF", entity.getExternalIdRelationship());
+ entity.setId(123L);
Item mappedBack = mapper.toItem(entity);
assertNotNull(mappedBack);
assertEquals("123", mappedBack.getPutCode());
From a64d9e1a0d3b11b48ef3a4d501852d50d34f9242 Mon Sep 17 00:00:00 2001
From: camdum <62257307+Camelia-Orcid@users.noreply.github.com>
Date: Thu, 8 Oct 2026 14:38:35 +0100
Subject: [PATCH 5/7] PD-14448 Fixed the JPQL entity property mapping in
ClientDetailsDaoImpl.findMVPEnabled() and corrected the client ID column
identifier in ClientDetailsDaoImpl.updateNotificationInfo()
Updated the JPQL query in ClientDetailsDaoImpl.findMVPEnabled() to reference the entity attribute clientType instead of the database column name client_type so that it complies with Hibernate 6 / Spring 6 query syntax. Corrected the primary key column name in the native SQL query within ClientDetailsDaoImpl.updateNotificationInfo() to client_details_id. Added unit test testFindMVPEnabled() in ClientDetailsDaoTest to verify that findMVPEnabled() successfully retrieves MVP-enabled clients.
---
.../dao/impl/ClientDetailsDaoImpl.java | 25 ++++++++-----------
.../persistence/dao/ClientDetailsDaoTest.java | 18 ++++++++++++-
2 files changed, 28 insertions(+), 15 deletions(-)
diff --git a/orcid-persistence/src/main/java/org/orcid/persistence/dao/impl/ClientDetailsDaoImpl.java b/orcid-persistence/src/main/java/org/orcid/persistence/dao/impl/ClientDetailsDaoImpl.java
index 11d0ac2b613..ba95e030c93 100644
--- a/orcid-persistence/src/main/java/org/orcid/persistence/dao/impl/ClientDetailsDaoImpl.java
+++ b/orcid-persistence/src/main/java/org/orcid/persistence/dao/impl/ClientDetailsDaoImpl.java
@@ -100,9 +100,8 @@ public void updateLastModified(String clientId) {
/**
* Update the last modified dates of given client ids
- *
- * @param clientIds
- * A list of client ids
+ *
+ * @param clientIds A list of client ids
* @return the amount of modified rows
*/
@@ -194,9 +193,8 @@ public void removeClient(String clientId) {
/**
* Get the public client that belongs to the given orcid ID
- *
- * @param ownerId
- * The user or group id
+ *
+ * @param ownerId The user or group id
* @return the public client that belongs to the given user
*/
@Override
@@ -216,9 +214,8 @@ public ClientDetailsEntity getPublicClient(String ownerId) {
/**
* Get member name
- *
- * @param clientId
- * The client id
+ *
+ * @param clientId The client id
* @return the name of the member owner of the given client
*/
@Override
@@ -269,7 +266,7 @@ public List findLegacyClientIds() {
@Transactional
public void changePersistenceTokensProperty(String clientId, boolean isPersistenTokensEnabled) {
Query updateQuery = entityManager
- .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.persistentTokensEnabled = :isPersistenTokensEnabled where c.id = :clientId");
+ .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.persistentTokensEnabled = :isPersistenTokensEnabled where c.id = :clientId");
updateQuery.setParameter("clientId", clientId);
updateQuery.setParameter("lastModified", new Date());
updateQuery.setParameter("isPersistenTokensEnabled", isPersistenTokensEnabled);
@@ -280,7 +277,7 @@ public void changePersistenceTokensProperty(String clientId, boolean isPersisten
@Transactional
public void activateClient(String clientDetailsId) {
Query updateQuery = entityManager
- .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.deactivatedDate = null, c.deactivatedBy = null where c.id = :clientId");
+ .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.deactivatedDate = null, c.deactivatedBy = null where c.id = :clientId");
updateQuery.setParameter("clientId", clientDetailsId);
updateQuery.setParameter("lastModified", new Date());
updateQuery.executeUpdate();
@@ -290,7 +287,7 @@ public void activateClient(String clientDetailsId) {
@Transactional
public void deactivateClient(String clientDetailsId, String deactivatedBy) {
Query updateQuery = entityManager
- .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.deactivatedDate = :deactivatedDate, c.deactivatedBy = :deactivatedBy where c.id = :clientId");
+ .createQuery("update ClientDetailsEntity c set c.lastModified = :lastModified, c.deactivatedDate = :deactivatedDate, c.deactivatedBy = :deactivatedBy where c.id = :clientId");
updateQuery.setParameter("clientId", clientDetailsId);
Date now = new Date();
updateQuery.setParameter("lastModified", now);
@@ -324,7 +321,7 @@ public boolean updateClientGrantedAuthority(String clientId, String grantedAutho
@Transactional
public boolean updateNotificationInfo(String clientId, boolean userNotificationEnabled, String notificationWebpageUrl, String notificationDomains) {
Query updateNotificationInfoQuery = entityManager.createNativeQuery(
- "UPDATE client_details SET user_notification_enabled= :userNotificationEnabled, notification_webpage_url= :notificationWebpageUrl, notification_domains= :notificationDomains WHERE id = :clientId");
+ "UPDATE client_details SET user_notification_enabled= :userNotificationEnabled, notification_webpage_url= :notificationWebpageUrl, notification_domains= :notificationDomains WHERE client_details_id = :clientId");
updateNotificationInfoQuery.setParameter("clientId", clientId);
updateNotificationInfoQuery.setParameter("userNotificationEnabled", userNotificationEnabled);
updateNotificationInfoQuery.setParameter("notificationWebpageUrl", notificationWebpageUrl);
@@ -336,7 +333,7 @@ public boolean updateNotificationInfo(String clientId, boolean userNotificationE
@Transactional(value = "transactionManagerReadOnly", readOnly = true)
@SuppressWarnings("unchecked")
public List findMVPEnabled() {
- Query query = entityManager.createQuery("from ClientDetailsEntity where userNotificationEnabled = :userNotificationEnabled and client_type = :premiumUpdater" );
+ Query query = entityManager.createQuery("from ClientDetailsEntity where userNotificationEnabled = :userNotificationEnabled and clientType = :premiumUpdater");
query.setParameter("userNotificationEnabled", true);
query.setParameter("premiumUpdater", ClientType.PREMIUM_UPDATER.name());
return query.getResultList();
diff --git a/orcid-persistence/src/test/java/org/orcid/persistence/dao/ClientDetailsDaoTest.java b/orcid-persistence/src/test/java/org/orcid/persistence/dao/ClientDetailsDaoTest.java
index b784aa911dc..d5b514a993e 100644
--- a/orcid-persistence/src/test/java/org/orcid/persistence/dao/ClientDetailsDaoTest.java
+++ b/orcid-persistence/src/test/java/org/orcid/persistence/dao/ClientDetailsDaoTest.java
@@ -1,6 +1,8 @@
package org.orcid.persistence.dao;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
@@ -12,12 +14,14 @@
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.orcid.jaxb.model.clientgroup.ClientType;
+import org.orcid.persistence.jpa.entities.ClientDetailsEntity;
import org.orcid.test.DBUnitTest;
import org.orcid.test.OrcidJUnit4ClassRunner;
import org.springframework.test.context.ContextConfiguration;
@RunWith(OrcidJUnit4ClassRunner.class)
-@ContextConfiguration(locations = { "classpath:test-orcid-persistence-context.xml" })
+@ContextConfiguration(locations = {"classpath:test-orcid-persistence-context.xml"})
public class ClientDetailsDaoTest extends DBUnitTest {
private static String CLIENT_ID = "APP-5555555555555557";
@@ -48,4 +52,16 @@ public void testUpdateLastModifiedBulk() {
assertEquals(updateClients, 2);
}
+ @Test
+ public void testFindMVPEnabled() {
+ clientDetailsDao.updateClientType(ClientType.PREMIUM_UPDATER.name(), CLIENT_ID);
+ clientDetailsDao.updateNotificationInfo(CLIENT_ID, true, "http://test.com", "test.com");
+ List mvpEnabledClients = clientDetailsDao.findMVPEnabled();
+ assertNotNull(mvpEnabledClients);
+ assertEquals(1, mvpEnabledClients.size());
+ assertEquals(CLIENT_ID, mvpEnabledClients.get(0).getId());
+ assertEquals(ClientType.PREMIUM_UPDATER.name(), mvpEnabledClients.get(0).getClientType());
+ assertTrue(mvpEnabledClients.get(0).isUserNotificationEnabled());
+ }
+
}
From 6c58bb23d58a894469f079cc88e1c8099fbe3642 Mon Sep 17 00:00:00 2001
From: amontenegro
Date: Wed, 7 Oct 2026 14:10:21 -0600
Subject: [PATCH 6/7] Use the latest version of togglz-junit
---
orcid-api-web/pom.xml | 7 -------
1 file changed, 7 deletions(-)
diff --git a/orcid-api-web/pom.xml b/orcid-api-web/pom.xml
index 2379d667abe..d209cabb951 100644
--- a/orcid-api-web/pom.xml
+++ b/orcid-api-web/pom.xml
@@ -67,13 +67,6 @@
-
-
- org.togglz
- togglz-junit
- 2.4.1.Final
- test
-
${project.groupId}
orcid-test
From 2648d1168279fca8fef1a183040aceda5f8e39f7 Mon Sep 17 00:00:00 2001
From: github actions
Date: Fri, 9 Oct 2026 21:59:53 +0000
Subject: [PATCH 7/7] v3.23.24 changelog update
---
CHANGELOG.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f918a6bc35e..e3cff39b8d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## v3.23.24 - 2026-10-09
+
+[Full Changelog](https://github.com/ORCID/ORCID-Source/compare/v3.24.8...v3.23.24)
+
## v3.23.22 - 2026-10-05
[Full Changelog](https://github.com/ORCID/ORCID-Source/compare/v3.23.21...v3.23.22)