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 serializer = beanClassLoaderAware.springSessionDefaultRedisSerializer(); + Map sessionDelta = new HashMap<>(); + sessionDelta.put("lastAccessedTime", 123456789L); + + byte[] json = serializer.serialize(sessionDelta); + Map deserialized = (Map) serializer.deserialize(json); + + assertEquals(Long.valueOf(123456789L), deserialized.get("lastAccessedTime")); + } } From bc6c1d68e2d79487a539525c7357bfdf1a85ad70 Mon Sep 17 00:00:00 2001 From: camdum <62257307+Camelia-Orcid@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:54:42 +0100 Subject: [PATCH 3/7] PD-14452 Refactored notification item mapping and post-processing logic into NotificationMapperV2 and NotificationMapperV3, aligning with the existing mapper architectural pattern. Consolidated item mappings (Item <-> NotificationItemEntity), external identifier mappings, and JSON additionalInfo conversions into NotificationMapperV2 and NotificationMapperV3. Added @AfterMapping post-processing hooks in NotificationMapperV2 and NotificationMapperV3 to guarantee non-null Items containers and null-safe external identifier instances. Updated JpaJaxbNotificationAdapterImpl (v2 and v3) to delegate item mapping and notification post-processing to NotificationMapperV2 and NotificationMapperV3 via Spring component injection. Added comprehensive unit tests in NotificationMapperV2Test and NotificationMapperV3Test verifying bidirectional item mapping and Items initialization. --- .../mapstruct/NotificationMapperV2.java | 57 ++++++++++++- .../mapstruct/NotificationMapperV3.java | 62 +++++++++++++- .../impl/JpaJaxbNotificationAdapterImpl.java | 77 ++++++----------- .../impl/JpaJaxbNotificationAdapterImpl.java | 77 ++++++----------- .../org/orcid/core/template/digest_email.ftl | 5 ++ .../orcid/core/template/digest_email_html.ftl | 2 + .../core/template/digest_notification.ftl | 5 ++ .../template/digest_notification_html.ftl | 2 + .../mapstruct/NotificationMapperV2Test.java | 79 ++++++++++++++++++ .../mapstruct/NotificationMapperV3Test.java | 82 +++++++++++++++++++ .../JpaJaxbNotificationAdapterTest.java | 56 +++++++++++++ .../v3/JpaJaxbNotificationAdapterTest.java | 56 +++++++++++++ .../cli/manager/EmailMessageSenderTest.java | 26 ++++++ 13 files changed, 479 insertions(+), 107 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 4fa7f592d11..e5f5611032f 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 @@ -1,13 +1,21 @@ package org.orcid.core.adapter.mapstruct; +import java.util.HashMap; +import java.util.Map; + import org.apache.commons.lang3.StringUtils; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; import org.orcid.core.manager.IdentityProviderManager; import org.orcid.core.manager.impl.OrcidUrlManager; +import org.orcid.core.utils.JsonUtils; import org.orcid.jaxb.model.notification.amended_v2.NotificationAmended; import org.orcid.jaxb.model.notification.permission_v2.AuthorizationUrl; import org.orcid.jaxb.model.notification.permission_v2.Item; +import org.orcid.jaxb.model.notification.permission_v2.Items; import org.orcid.jaxb.model.notification.permission_v2.NotificationPermission; import org.orcid.jaxb.model.v3.release.notification.amended.AmendedSection; import org.orcid.model.notification.institutional_sign_in_v2.NotificationInstitutionalConnection; @@ -16,11 +24,51 @@ import org.orcid.persistence.jpa.entities.NotificationInstitutionalConnectionEntity; import org.orcid.persistence.jpa.entities.NotificationItemEntity; -@Mapper +@Mapper(componentModel = "spring", uses = { ExternalIdentifierTypeMapper.class, UrlMapperV2.class }) public interface NotificationMapperV2 { NotificationMapperV2 INSTANCE = Mappers.getMapper(NotificationMapperV2.class); + @Mapping(source = "putCode", target = "id") + @Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb") + @Mapping(source = "externalIdentifier.value", target = "externalIdValue") + @Mapping(source = "externalIdentifier.url", target = "externalIdUrl") + @Mapping(source = "externalIdentifier.relationship", target = "externalIdRelationship") + @Mapping(source = "additionalInfo", target = "additionalInfo") + NotificationItemEntity toNotificationItemEntity(Item item); + + @Mapping(source = "id", target = "putCode") + @Mapping(source = "externalIdType", target = "externalIdentifier.type", qualifiedByName = "dbToApi") + @Mapping(source = "externalIdValue", target = "externalIdentifier.value") + @Mapping(source = "externalIdUrl", target = "externalIdentifier.url") + @Mapping(source = "externalIdRelationship", target = "externalIdentifier.relationship") + @Mapping(source = "additionalInfo", target = "additionalInfo") + Item toItem(NotificationItemEntity entity); + + @AfterMapping + default void afterToItem(NotificationItemEntity entity, @MappingTarget Item item) { + if (StringUtils.isBlank(entity.getExternalIdType()) && StringUtils.isBlank(entity.getExternalIdValue()) + && StringUtils.isBlank(entity.getExternalIdUrl()) && StringUtils.isBlank(entity.getExternalIdRelationship())) { + item.setExternalIdentifier(null); + } + } + + @SuppressWarnings("rawtypes") + default String mapAdditionalInfo(Map map) { + if (map == null || map.isEmpty()) { + return null; + } + return JsonUtils.convertToJsonString(map); + } + + @SuppressWarnings("rawtypes") + default Map mapAdditionalInfo(String json) { + if (StringUtils.isBlank(json)) { + return null; + } + return JsonUtils.readObjectFromJsonString(json, HashMap.class); + } + default String buildAuthorizationUrlIfBlank(String existingUrl, String path, OrcidUrlManager orcidUrlManager) { if (StringUtils.isBlank(existingUrl)) { return orcidUrlManager.getBaseUrl() + path; @@ -34,6 +82,9 @@ default void mapPermissionBtoA(NotificationAddItemsEntity entity, NotificationPe authUrl.setPath(fullPath); authUrl.setHost(baseHost); } + if (notification.getItems() == null) { + notification.setItems(new Items()); + } } default void mapInstitutionalBtoA(NotificationInstitutionalConnectionEntity entity, NotificationInstitutionalConnection notification, String fullPath, @@ -66,7 +117,6 @@ default void mapAmendedAtoB(NotificationAmended model, NotificationAmendedEntity if (model.getAmendedSection() == null) { return; } - switch (model.getAmendedSection()) { case AFFILIATION: entity.setAmendedSection(AmendedSection.AFFILIATION.name()); @@ -108,6 +158,9 @@ default void mapAmendedAtoB(NotificationAmended model, NotificationAmendedEntity } default void mapAmendedBtoA(NotificationAmendedEntity entity, NotificationAmended model) { + if (model.getItems() == null) { + model.setItems(new Items()); + } if (entity.getAmendedSection() == null) { return; } 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 dcacdd647d8..ad2ddc347f3 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 @@ -1,24 +1,75 @@ package org.orcid.core.adapter.mapstruct; +import java.util.HashMap; +import java.util.Map; + import org.apache.commons.lang3.StringUtils; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; import org.orcid.core.manager.IdentityProviderManager; +import org.orcid.core.utils.JsonUtils; +import org.orcid.jaxb.model.v3.release.notification.amended.NotificationAmended; import org.orcid.jaxb.model.v3.release.notification.permission.AuthorizationUrl; import org.orcid.jaxb.model.v3.release.notification.permission.Item; +import org.orcid.jaxb.model.v3.release.notification.permission.Items; import org.orcid.jaxb.model.v3.release.notification.permission.NotificationPermission; import org.orcid.model.v3.release.notification.institutional_sign_in.NotificationInstitutionalConnection; import org.orcid.model.v3.release.notification.internal.NotificationFindMyStuff; import org.orcid.persistence.jpa.entities.NotificationAddItemsEntity; +import org.orcid.persistence.jpa.entities.NotificationAmendedEntity; import org.orcid.persistence.jpa.entities.NotificationFindMyStuffEntity; import org.orcid.persistence.jpa.entities.NotificationInstitutionalConnectionEntity; import org.orcid.persistence.jpa.entities.NotificationItemEntity; +import org.orcid.pojo.ajaxForm.PojoUtil; -@Mapper +@Mapper(componentModel = "spring", uses = { ExternalIdentifierTypeMapper.class, UrlMapperV3.class }) public interface NotificationMapperV3 { NotificationMapperV3 INSTANCE = Mappers.getMapper(NotificationMapperV3.class); + @Mapping(source = "putCode", target = "id") + @Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb") + @Mapping(source = "externalIdentifier.value", target = "externalIdValue") + @Mapping(source = "externalIdentifier.url", target = "externalIdUrl") + @Mapping(source = "externalIdentifier.relationship", target = "externalIdRelationship") + @Mapping(source = "additionalInfo", target = "additionalInfo") + NotificationItemEntity toNotificationItemEntity(Item item); + + @Mapping(source = "id", target = "putCode") + @Mapping(source = "externalIdType", target = "externalIdentifier.type", qualifiedByName = "dbToApi") + @Mapping(source = "externalIdValue", target = "externalIdentifier.value") + @Mapping(source = "externalIdUrl", target = "externalIdentifier.url") + @Mapping(source = "externalIdRelationship", target = "externalIdentifier.relationship") + @Mapping(source = "additionalInfo", target = "additionalInfo") + Item toItem(NotificationItemEntity entity); + + @AfterMapping + default void afterToItem(NotificationItemEntity entity, @MappingTarget Item item) { + if (StringUtils.isBlank(entity.getExternalIdType()) && StringUtils.isBlank(entity.getExternalIdValue()) + && StringUtils.isBlank(entity.getExternalIdUrl()) && StringUtils.isBlank(entity.getExternalIdRelationship())) { + item.setExternalIdentifier(null); + } + } + + @SuppressWarnings("rawtypes") + default String mapAdditionalInfo(Map map) { + if (map == null || map.isEmpty()) { + return null; + } + return JsonUtils.convertToJsonString(map); + } + + @SuppressWarnings("rawtypes") + default Map mapAdditionalInfo(String json) { + if (PojoUtil.isEmpty(json)) { + return null; + } + return JsonUtils.readObjectFromJsonString(json, HashMap.class); + } + default String buildAuthorizationUrlIfBlank(String existingUrl, String path, String baseUrl) { if (StringUtils.isBlank(existingUrl)) { return baseUrl + path; @@ -32,6 +83,9 @@ default void mapPermissionBtoA(NotificationAddItemsEntity entity, NotificationPe authUrl.setPath(fullPath); authUrl.setHost(baseHost); } + if (notification.getItems() == null) { + notification.setItems(new Items()); + } } default void mapInstitutionalBtoA(NotificationInstitutionalConnectionEntity entity, NotificationInstitutionalConnection notification, String fullPath, @@ -50,6 +104,12 @@ default void mapInstitutionalBtoA(NotificationInstitutionalConnectionEntity enti } } + default void mapAmendedBtoA(NotificationAmendedEntity entity, NotificationAmended notification) { + if (notification.getItems() == null) { + notification.setItems(new Items()); + } + } + default void mapFindMyStuffAtoB(NotificationFindMyStuff notification, NotificationFindMyStuffEntity entity, String existingAuthorizationUrl, String builtAuthorizationUrl) { if (StringUtils.isBlank(existingAuthorizationUrl)) { diff --git a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/impl/JpaJaxbNotificationAdapterImpl.java b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/impl/JpaJaxbNotificationAdapterImpl.java index 5a8bf94f267..a5bb5663381 100644 --- a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/impl/JpaJaxbNotificationAdapterImpl.java +++ b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/impl/JpaJaxbNotificationAdapterImpl.java @@ -22,7 +22,9 @@ import org.orcid.core.adapter.JpaJaxbNotificationAdapter; import org.orcid.core.adapter.mapstruct.ExternalIdentifierTypeMapper; +import org.orcid.core.adapter.mapstruct.NotificationMapperV2; import org.orcid.core.adapter.mapstruct.SourceMapperV2; +import org.orcid.core.adapter.mapstruct.UrlMapperV2; import org.orcid.core.exception.OrcidValidationException; import org.orcid.core.manager.IdentityProviderManager; import org.orcid.core.manager.impl.OrcidUrlManager; @@ -34,6 +36,7 @@ import org.orcid.jaxb.model.notification.custom_v2.NotificationTip; import org.orcid.jaxb.model.notification.permission_v2.AuthorizationUrl; import org.orcid.jaxb.model.notification.permission_v2.Item; +import org.orcid.jaxb.model.notification.permission_v2.Items; import org.orcid.jaxb.model.notification.permission_v2.NotificationPermission; import org.orcid.jaxb.model.notification_v2.Notification; import org.orcid.model.notification.institutional_sign_in_v2.NotificationInstitutionalConnection; @@ -44,12 +47,15 @@ */ @Mapper( componentModel = "spring", - uses = {SourceMapperV2.class, ExternalIdentifierTypeMapper.class} + uses = {SourceMapperV2.class, NotificationMapperV2.class, ExternalIdentifierTypeMapper.class, UrlMapperV2.class} ) public abstract class JpaJaxbNotificationAdapterImpl implements JpaJaxbNotificationAdapter { private static final String LAST_RESORT_IDENTITY_PROVIDER_NAME = "identity provider"; + @Autowired + protected NotificationMapperV2 notificationMapperV2; + @Autowired protected OrcidUrlManager orcidUrlManager; @@ -158,11 +164,11 @@ protected void afterMapPermission(NotificationPermission n, @MappingTarget Notif @AfterMapping protected void afterMapPermissionEntity(NotificationAddItemsEntity entity, @MappingTarget NotificationPermission n) { - AuthorizationUrl authUrl = n.getAuthorizationUrl(); - if (authUrl != null && authUrl.getUri() != null) { - authUrl.setPath(extractFullPath(authUrl.getUri())); - authUrl.setHost(orcidUrlManager.getBaseHost()); + String fullPath = null; + if (n.getAuthorizationUrl() != null && n.getAuthorizationUrl().getUri() != null) { + fullPath = extractFullPath(n.getAuthorizationUrl().getUri()); } + notificationMapperV2.mapPermissionBtoA(entity, n, fullPath, orcidUrlManager.getBaseHost()); } // 6. Notification Institutional Connection @@ -188,71 +194,36 @@ protected void afterMapInstitutionalConnection(NotificationInstitutionalConnecti @AfterMapping protected void afterMapInstitutionalConnectionEntity(NotificationInstitutionalConnectionEntity entity, @MappingTarget NotificationInstitutionalConnection n) { - AuthorizationUrl authUrl = n.getAuthorizationUrl(); - if (authUrl != null && authUrl.getUri() != null) { - authUrl.setPath(extractFullPath(authUrl.getUri())); - authUrl.setHost(orcidUrlManager.getBaseHost()); - } - - String providerId = entity.getAuthenticationProviderId(); - if (StringUtils.isNotBlank(providerId)) { - String idpName = identityProviderManager.retrieveIdentitifyProviderName(providerId); - n.setIdpName(idpName); - } else { - n.setIdpName(LAST_RESORT_IDENTITY_PROVIDER_NAME); + String fullPath = null; + if (n.getAuthorizationUrl() != null && n.getAuthorizationUrl().getUri() != null) { + fullPath = extractFullPath(n.getAuthorizationUrl().getUri()); } + notificationMapperV2.mapInstitutionalBtoA(entity, n, fullPath, orcidUrlManager.getBaseHost(), identityProviderManager, + LAST_RESORT_IDENTITY_PROVIDER_NAME); } // 7. Notification Amended @Mapping(source = "putCode", target = "id") @Mapping(target = "dateCreated", ignore = true) @Mapping(source = "items.items", target = "notificationItems") - @Mapping(source = "amendedSection", target = "amendedSection") + @Mapping(target = "amendedSection", ignore = true) protected abstract NotificationAmendedEntity map(NotificationAmended n); @Mapping(source = "id", target = "putCode") @Mapping(source = "dateCreated", target = "createdDate") @Mapping(source = "notificationItems", target = "items.items") - @Mapping(source = "amendedSection", target = "amendedSection") + @Mapping(target = "amendedSection", ignore = true) @Mapping(source = ".", target = "source") protected abstract NotificationAmended map(NotificationAmendedEntity e); - // Custom AmendedSection Enum Converters - protected String mapAmendedSection(org.orcid.jaxb.model.notification.amended_v2.AmendedSection section) { - if (section == null) return org.orcid.jaxb.model.v3.release.notification.amended.AmendedSection.UNKNOWN.name(); - return section.name(); - } - - protected org.orcid.jaxb.model.notification.amended_v2.AmendedSection mapAmendedSection(String section) { - if (StringUtils.isBlank(section)) return org.orcid.jaxb.model.notification.amended_v2.AmendedSection.UNKNOWN; - try { - return org.orcid.jaxb.model.notification.amended_v2.AmendedSection.valueOf(section); - } catch (IllegalArgumentException e) { - return org.orcid.jaxb.model.notification.amended_v2.AmendedSection.AFFILIATION; - } - } - - // Notification Items Mapping - @Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb") - @Mapping(source = "externalIdentifier.value", target = "externalIdValue") - @Mapping(source = "additionalInfo", target = "additionalInfo") - protected abstract NotificationItemEntity mapItem(Item item); - - @Mapping(source = "externalIdType", target = "externalIdentifier.type", qualifiedByName = "dbToApi") - @Mapping(source = "externalIdValue", target = "externalIdentifier.value") - @Mapping(source = "additionalInfo", target = "additionalInfo") - protected abstract Item mapItem(NotificationItemEntity entity); - - @SuppressWarnings("rawtypes") - protected String mapAdditionalInfo(Map map) { - if (map == null || map.isEmpty()) return null; - return JsonUtils.convertToJsonString(map); + @AfterMapping + protected void afterMapAmended(NotificationAmended n, @MappingTarget NotificationAmendedEntity entity) { + notificationMapperV2.mapAmendedAtoB(n, entity); } - @SuppressWarnings("rawtypes") - protected Map mapAdditionalInfo(String json) { - if (StringUtils.isBlank(json)) return null; - return JsonUtils.readObjectFromJsonString(json, HashMap.class); + @AfterMapping + protected void afterMapAmendedEntity(NotificationAmendedEntity entity, @MappingTarget NotificationAmended n) { + notificationMapperV2.mapAmendedBtoA(entity, n); } // XMLGregorianCalendar <-> java.util.Date Converters diff --git a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/v3/impl/JpaJaxbNotificationAdapterImpl.java b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/v3/impl/JpaJaxbNotificationAdapterImpl.java index 89c319150b9..a0b26be42b5 100644 --- a/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/v3/impl/JpaJaxbNotificationAdapterImpl.java +++ b/orcid-core/src/main/java/org/orcid/core/adapter/mapstruct/v3/impl/JpaJaxbNotificationAdapterImpl.java @@ -20,10 +20,11 @@ import org.mapstruct.MappingTarget; import org.springframework.beans.factory.annotation.Autowired; +import org.orcid.core.adapter.v3.JpaJaxbNotificationAdapter; import org.orcid.core.adapter.mapstruct.ExternalIdentifierTypeMapper; +import org.orcid.core.adapter.mapstruct.NotificationMapperV3; import org.orcid.core.adapter.mapstruct.SourceMapperV3; import org.orcid.core.adapter.mapstruct.UrlMapperV3; -import org.orcid.core.adapter.v3.JpaJaxbNotificationAdapter; import org.orcid.core.exception.OrcidValidationException; import org.orcid.core.manager.IdentityProviderManager; import org.orcid.core.manager.impl.OrcidUrlManager; @@ -36,6 +37,7 @@ import org.orcid.jaxb.model.v3.release.notification.custom.NotificationTip; import org.orcid.jaxb.model.v3.release.notification.permission.AuthorizationUrl; import org.orcid.jaxb.model.v3.release.notification.permission.Item; +import org.orcid.jaxb.model.v3.release.notification.permission.Items; import org.orcid.jaxb.model.v3.release.notification.permission.NotificationPermission; import org.orcid.model.v3.release.notification.institutional_sign_in.NotificationInstitutionalConnection; import org.orcid.model.v3.release.notification.internal.NotificationFindMyStuff; @@ -53,12 +55,15 @@ @Mapper( componentModel = "spring", - uses = {SourceMapperV3.class, ExternalIdentifierTypeMapper.class, UrlMapperV3.class} + uses = {SourceMapperV3.class, NotificationMapperV3.class, ExternalIdentifierTypeMapper.class, UrlMapperV3.class} ) public abstract class JpaJaxbNotificationAdapterImpl implements JpaJaxbNotificationAdapter { private static final String LAST_RESORT_IDENTITY_PROVIDER_NAME = "identity provider"; + @Autowired + protected NotificationMapperV3 notificationMapperV3; + @Autowired protected OrcidUrlManager orcidUrlManager; @@ -169,11 +174,11 @@ protected void afterMapPermission(NotificationPermission n, @MappingTarget Notif @AfterMapping protected void afterMapPermissionEntity(NotificationAddItemsEntity entity, @MappingTarget NotificationPermission n) { - AuthorizationUrl authUrl = n.getAuthorizationUrl(); - if (authUrl != null && authUrl.getUri() != null) { - authUrl.setPath(extractFullPath(authUrl.getUri())); - authUrl.setHost(orcidUrlManager.getBaseHost()); + String fullPath = null; + if (n.getAuthorizationUrl() != null && n.getAuthorizationUrl().getUri() != null) { + fullPath = extractFullPath(n.getAuthorizationUrl().getUri()); } + notificationMapperV3.mapPermissionBtoA(entity, n, fullPath, orcidUrlManager.getBaseHost()); } // 6. Notification Institutional Connection @@ -199,19 +204,12 @@ protected void afterMapInstitutionalConnection(NotificationInstitutionalConnecti @AfterMapping protected void afterMapInstitutionalConnectionEntity(NotificationInstitutionalConnectionEntity entity, @MappingTarget NotificationInstitutionalConnection n) { - AuthorizationUrl authUrl = n.getAuthorizationUrl(); - if (authUrl != null && authUrl.getUri() != null) { - authUrl.setPath(extractFullPath(authUrl.getUri())); - authUrl.setHost(orcidUrlManager.getBaseHost()); - } - - String providerId = entity.getAuthenticationProviderId(); - if (StringUtils.isNotBlank(providerId)) { - String idpName = identityProviderManager.retrieveIdentitifyProviderName(providerId); - n.setIdpName(idpName); - } else { - n.setIdpName(LAST_RESORT_IDENTITY_PROVIDER_NAME); + String fullPath = null; + if (n.getAuthorizationUrl() != null && n.getAuthorizationUrl().getUri() != null) { + fullPath = extractFullPath(n.getAuthorizationUrl().getUri()); } + notificationMapperV3.mapInstitutionalBtoA(entity, n, fullPath, orcidUrlManager.getBaseHost(), identityProviderManager, + LAST_RESORT_IDENTITY_PROVIDER_NAME); } // 7. Notification Amended @@ -228,6 +226,11 @@ protected void afterMapInstitutionalConnectionEntity(NotificationInstitutionalCo @Mapping(source = ".", target = "source") protected abstract NotificationAmended map(NotificationAmendedEntity e); + @AfterMapping + protected void afterMapAmendedEntity(NotificationAmendedEntity entity, @MappingTarget NotificationAmended n) { + notificationMapperV3.mapAmendedBtoA(entity, n); + } + // 8. Notification Find My Stuff (V3 specific) @Mapping(source = "putCode", target = "id") @Mapping(target = "dateCreated", ignore = true) @@ -247,17 +250,16 @@ protected void afterMapFindMyStuff(NotificationFindMyStuff n, @MappingTarget Not validateAndConvertToURI(authUrl); entity.setAuthorizationUrl(authUrl); } - entity.setAuthenticationProviderId(n.getServiceProviderId()); + notificationMapperV3.mapFindMyStuffAtoB(n, entity, entity.getAuthorizationUrl(), entity.getAuthorizationUrl()); } @AfterMapping protected void afterMapFindMyStuffEntity(NotificationFindMyStuffEntity entity, @MappingTarget NotificationFindMyStuff n) { - AuthorizationUrl authUrl = n.getAuthorizationUrl(); - if (authUrl != null && authUrl.getUri() != null) { - authUrl.setPath(extractFullPath(authUrl.getUri())); - authUrl.setHost(orcidUrlManager.getBaseHost()); + String fullPath = null; + if (n.getAuthorizationUrl() != null && n.getAuthorizationUrl().getUri() != null) { + fullPath = extractFullPath(n.getAuthorizationUrl().getUri()); } - n.setServiceProviderId(entity.getAuthenticationProviderId()); + notificationMapperV3.mapFindMyStuffBtoA(entity, n, fullPath, orcidUrlManager.getBaseHost()); } // Custom AmendedSection Enum Converters @@ -275,33 +277,6 @@ protected org.orcid.jaxb.model.v3.release.notification.amended.AmendedSection ma } } - // Notification Items Mapping - @Mapping(source = "externalIdentifier.type", target = "externalIdType", qualifiedByName = "apiToDb") - @Mapping(source = "externalIdentifier.value", target = "externalIdValue") - @Mapping(source = "externalIdentifier.url", target = "externalIdUrl") - @Mapping(source = "externalIdentifier.relationship", target = "externalIdRelationship") - @Mapping(source = "additionalInfo", target = "additionalInfo") - protected abstract NotificationItemEntity mapItem(Item item); - - @Mapping(source = "externalIdType", target = "externalIdentifier.type", qualifiedByName = "dbToApi") - @Mapping(source = "externalIdValue", target = "externalIdentifier.value") - @Mapping(source = "externalIdUrl", target = "externalIdentifier.url") - @Mapping(source = "externalIdRelationship", target = "externalIdentifier.relationship") - @Mapping(source = "additionalInfo", target = "additionalInfo") - protected abstract Item mapItem(NotificationItemEntity entity); - - @SuppressWarnings("rawtypes") - protected String mapAdditionalInfo(Map map) { - if (map == null || map.isEmpty()) return null; - return JsonUtils.convertToJsonString(map); - } - - @SuppressWarnings("rawtypes") - protected Map mapAdditionalInfo(String json) { - if (PojoUtil.isEmpty(json)) return null; - return JsonUtils.readObjectFromJsonString(json, HashMap.class); - } - // XMLGregorianCalendar <-> java.util.Date Converters protected Date xmlGregorianCalendarToDate(XMLGregorianCalendar cal) { if (cal == null) { diff --git a/orcid-core/src/main/resources/org/orcid/core/template/digest_email.ftl b/orcid-core/src/main/resources/org/orcid/core/template/digest_email.ftl index d6a49feefcb..74448651abf 100644 --- a/orcid-core/src/main/resources/org/orcid/core/template/digest_email.ftl +++ b/orcid-core/src/main/resources/org/orcid/core/template/digest_email.ftl @@ -19,6 +19,7 @@ <#if notificationType == 'PERMISSION'> ${(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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c})<#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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}) +<#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}) @@ -29,6 +30,10 @@ ${(digestEmail.notificationsBySourceId[sourceId].source.sourceName.content)!sour <@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} +<#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} + <#elseif notificationType == 'AMENDED' && !verboseNotifications> <#assign amendedSection><@emailMacros.msg "email.common.recordsection." + notification.amendedSection /> <@emailMacros.msg "email.digest.hasupdated_1" /> diff --git a/orcid-core/src/main/resources/org/orcid/core/template/digest_email_html.ftl b/orcid-core/src/main/resources/org/orcid/core/template/digest_email_html.ftl index 0e836a77555..a91034a3a41 100644 --- a/orcid-core/src/main/resources/org/orcid/core/template/digest_email_html.ftl +++ b/orcid-core/src/main/resources/org/orcid/core/template/digest_email_html.ftl @@ -32,6 +32,7 @@ <#if notificationType == 'PERMISSION'>

 ${(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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}) <#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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c})
+ <#if notification.items??> <#assign itemsByType=notification.items.itemsByType> <#list itemsByType?keys?sort as itemType> @@ -65,6 +66,7 @@
+

<#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} ${(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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c})<#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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c}) + <#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} + <#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} + <#elseif notificationType == 'AMENDED' && !verboseNotifications> <#assign amendedSection><@emailMacros.msg "email.common.recordsection." + notification.amendedSection /> 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 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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c})<#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 notification.createdDate.day?string?length == 1>0${notification.createdDate.day?c}<#else>${notification.createdDate.day?c})

+ <#if notification.items??> <#assign itemsByType=notification.items.itemsByType> <#list itemsByType?keys?sort as itemType>
@@ -125,6 +126,7 @@
+ 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)