Skip to content

Commit baa9dca

Browse files
committed
Merge remote-tracking branch 'IQSS/develop' into
GDCC/8914-COAR-compliant_messaging
2 parents 506896f + 739052f commit baa9dca

File tree

8 files changed

+284
-239
lines changed

8 files changed

+284
-239
lines changed

doc/release-notes/10490-COAR-Notify.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
### Support for COAR Notify Relationship Announcement
22

3-
Dataverse now supports sending and recieving [Linked Data Notification ](https://www.w3.org/TR/ldn/) messages involved in the
3+
Dataverse now supports sending and receiving [Linked Data Notification ](https://www.w3.org/TR/ldn/) messages involved in the
44
[COAR Notify Relationship Announcement Workflow](https://coar-notify.net/catalogue/workflows/repository-relationship-repository/).
55

66
Dataverse can send messages to configured repositories announcing that a dataset has a related publication (as defined in the dataset metadata). This may be done automatically upon publication or triggered manually by a superuser. The receiving repository may do anything with the message, with the default expectation being that the repository will create a backlink from the publication to the dataset (assuming the publication exists in the repository, admins agree the link makes sense, etc.)
77

8-
Conversely, Dataverse can recieve notices from other configured repositories announcing relationships between their publications and datasets. If the referenced dataset exists in the Dataverse instance, a notification will be sent to users who can publish the dataset, or, optionally only superusers who can publish the dataset. They can then decide whether to create a backlink to the publication in the dataset metadata.
8+
Conversely, Dataverse can receive notices from other configured repositories announcing relationships between their publications and datasets. If the referenced dataset exists in the Dataverse instance, a notification will be sent to users who can publish the dataset, or, optionally, only superusers who can publish the dataset. They can then decide whether to create a backlink to the publication in the dataset metadata.
99

1010
(Earlier releases of Dataverse had experimental support in this area that was based on message formats defined prior to finalization of the COAR Notify specification for relationship announcements.)
1111

1212
#### New Settings/JVM Options
1313

1414
Configuration for sending messages involves specifying the
15-
:COARNotifyRelationshipAnnpuncementTargets and :COARNotifyRelationshipAnnpuncementTriggerFields
15+
:COARNotifyRelationshipAnnouncementTargets and :COARNotifyRelationshipAnnouncementTriggerFields
1616

1717
Configuration to receive messages involves specifying
1818
DATAVERSE_LDN_ALLOWED_HOSTS (dataverse.ldn.allowed-hosts)

doc/sphinx-guides/source/api/linkeddatanotification.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Upon receipt of a relevant message, Dataverse will create Announcement Received
1111

1212
The ``dataverse.ldn.allowed-hosts`` JVM option is a comma-separated list of hosts from which Dataverse will accept and process messages. By default, no hosts are allowed. ``*`` can be used in testing to indicate all hosts are allowed.
1313

14-
The ``dataverse.ldn.coar-noptify.relationship-announcement.notify-superusers-only`` JVM option can be set to ``true`` to restrict notifications to superusers only (those who can publish the dataset). The default is to notify all users who can publish the dataset.
14+
The ``dataverse.ldn.coar-notify.relationship-announcement.notify-superusers-only`` JVM option can be set to ``true`` to restrict notifications to superusers only (those who can publish the dataset). The default is to notify all users who can publish the dataset.
1515

1616
Messages can be sent via POST, using the application/ld+json ContentType:
1717

doc/sphinx-guides/source/api/native-api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8241,7 +8241,7 @@ Get details of a workflow with a given id::
82418241
82428242
GET http://$SERVER/api/admin/workflows/$id
82438243
8244-
Add a new workflow. Request body specifies the workflow properties and steps in JSON format.
8244+
Add a new workflow. Request body specifies the workflow properties and steps in JSON format. Specifically, the body of the message should be a JSON Object with a String "name" for the workflow and a "steps" JSON Array containing a JSON Object per workflow step. (See :doc:`/developers/workflows` for the exiting steps and their required JSON representations.)
82458245
Sample ``json`` files are available at ``scripts/api/data/workflows/``::
82468246
82478247
POST http://$SERVER/api/admin/workflows

doc/sphinx-guides/source/installation/config.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3803,14 +3803,14 @@ Can also be set via any `supported MicroProfile Config API source`_, e.g. the en
38033803
Linked Data Notifications (LDN) Allowed Hosts
38043804
+++++++++++++++++++++++++++++++++++++++++++++
38053805

3806-
Dataverse supports receiving LDN notifications via the /api/inbox endpoint. The datavers.ldn.allowed-hosts allows you to specify the list of host IP addresses from which LDN notifications can be received, or ``*`` to receive messages from anywhere.
3806+
Dataverse supports receiving LDN notifications via the /api/inbox endpoint. The dataverse.ldn.allowed-hosts allows you to specify the list of host IP addresses from which LDN notifications can be received, or ``*`` to receive messages from anywhere.
38073807

38083808
Example: ``dataverse.ldn.allowed-hosts=*``
38093809

38103810
COAR Notify Relationship Announcement Notify Superusers Only
38113811
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
38123812

3813-
When Dataverse receives an LDN message conforming to the COAR Notify Relationship Announcement format and the message is about a Dataset hosted in the installation, Dataverse will send an notification to users who have permission to publish the dataset.
3813+
When Dataverse receives an LDN message conforming to the COAR Notify Relationship Announcement format and the message is about a dataset hosted in the installation, Dataverse will send an notification to users who have permission to publish the dataset.
38143814
This can instead be restricted to only superusers who can publish the dataset using this option.
38153815

38163816
Example: ``dataverse.coar-notify.relationship-announcement.notify-superusers-only=true``

src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public void processMessage(JsonObject msgObject) {
9696
Dataset dataset = datasetService.findByGlobalId(objectId);
9797
if (dataset == null) {
9898
logger.fine("Didn't find dataset for object ID: " + objectId + " - ignoring");
99+
return;
99100
}
100101

101102
// Create the citing resource JSON
Lines changed: 4 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,251 +1,24 @@
1-
21
package edu.harvard.iq.dataverse.api;
32

4-
import jakarta.json.Json;
5-
import jakarta.json.JsonObject;
6-
import jakarta.json.JsonObjectBuilder;
7-
import org.junit.jupiter.api.AfterAll;
8-
import org.junit.jupiter.api.BeforeAll;
93
import org.junit.jupiter.api.Test;
10-
import org.mockito.Mockito;
11-
12-
import edu.harvard.iq.dataverse.DataverseServiceBean;
13-
import edu.harvard.iq.dataverse.branding.BrandingUtil;
14-
import edu.harvard.iq.dataverse.settings.JvmSettings;
15-
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
16-
import edu.harvard.iq.dataverse.util.testing.JvmSetting;
17-
import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings;
18-
import edu.harvard.iq.dataverse.workflow.internalspi.COARNotifyRelationshipAnnouncementStep;
194
import static edu.harvard.iq.dataverse.workflow.internalspi.COARNotifyRelationshipAnnouncementStep.DATACITE_URI_PREFIX;
20-
import io.restassured.RestAssured;
215
import io.restassured.response.Response;
226

23-
import java.util.UUID;
24-
25-
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
267
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
27-
import static jakarta.ws.rs.core.Response.Status.OK;
28-
import static jakarta.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
29-
import static org.hamcrest.CoreMatchers.equalTo;
30-
import static org.hamcrest.CoreMatchers.notNullValue;
31-
import static org.junit.jupiter.api.Assertions.assertTrue;
328

33-
@LocalJvmSettings
34-
@JvmSetting(key = JvmSettings.LINKEDDATANOTIFICATION_ALLOWED_HOSTS, value = "*")
359
public class LDNInboxIT {
3610

37-
private static String apiToken;
38-
private static String username;
39-
private static String dataverseAlias;
40-
private static String datasetPid;
41-
private static Integer datasetId;
42-
private static String superuserApiToken;
43-
private static String superusername;
44-
45-
private static final String FRBR_SUPPLEMENT = "http://purl.org/vocab/frbr/core#supplement";
46-
47-
static SettingsServiceBean settingsServiceBean = Mockito.mock(SettingsServiceBean.class);
48-
static DataverseServiceBean dataverseServiceBean = Mockito.mock(DataverseServiceBean.class);
49-
50-
@BeforeAll
51-
public static void setUpClass() {
52-
RestAssured.baseURI = UtilIT.getRestAssuredBaseUri();
53-
54-
// Create test user and get API token
55-
Response createUser = UtilIT.createRandomUser();
56-
apiToken = UtilIT.getApiTokenFromResponse(createUser);
57-
username = UtilIT.getUsernameFromResponse(createUser);
58-
59-
// Create superuser for receiving notifications
60-
Response createSuperuser = UtilIT.createRandomUser();
61-
superuserApiToken = UtilIT.getApiTokenFromResponse(createSuperuser);
62-
superusername = UtilIT.getUsernameFromResponse(createSuperuser);
63-
Response makeSuperuser = UtilIT.setSuperuserStatus(superusername, true);
64-
makeSuperuser.then().assertThat().statusCode(OK.getStatusCode());
65-
66-
// Create dataverse
67-
Response createDataverse = UtilIT.createRandomDataverse(apiToken);
68-
createDataverse.then().assertThat().statusCode(201);
69-
dataverseAlias = UtilIT.getAliasFromResponse(createDataverse);
70-
71-
// Create and publish a dataset
72-
Response createDataset = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
73-
createDataset.then().assertThat().statusCode(201);
74-
datasetId = UtilIT.getDatasetIdFromResponse(createDataset);
75-
datasetPid = UtilIT.getDatasetPersistentIdFromResponse(createDataset);
76-
77-
// Publish the dataset
78-
Response publishDataverse = UtilIT.publishDataverseViaNativeApi(dataverseAlias, apiToken);
79-
publishDataverse.then().assertThat().statusCode(OK.getStatusCode());
80-
81-
Response publishDataset = UtilIT.publishDatasetViaNativeApi(datasetPid, "major", apiToken);
82-
publishDataset.then().assertThat().statusCode(OK.getStatusCode());
83-
84-
// Setup mocks behavior, inject as deps - needed to build announcements
85-
Mockito.when(settingsServiceBean.getValueForKey(SettingsServiceBean.Key.InstallationName))
86-
.thenReturn("LDN IT Tester");
87-
BrandingUtil.injectServices(dataverseServiceBean, settingsServiceBean);
88-
}
89-
90-
@AfterAll
91-
public static void afterClass() {
92-
// Clean up: remove test dataset and dataverse
93-
if (datasetId != null) {
94-
Response destroyDataset = UtilIT.destroyDataset(datasetId, apiToken);
95-
destroyDataset.then().assertThat().statusCode(OK.getStatusCode());
96-
}
97-
98-
if (dataverseAlias != null) {
99-
Response deleteDataverse = UtilIT.deleteDataverse(dataverseAlias, apiToken);
100-
deleteDataverse.then().assertThat().statusCode(OK.getStatusCode());
101-
}
102-
103-
// Delete test users
104-
Response deleteUser = UtilIT.deleteUser(username);
105-
deleteUser.then().assertThat().statusCode(OK.getStatusCode());
106-
Response deleteSuperuser = UtilIT.deleteUser(superusername);
107-
deleteSuperuser.then().assertThat().statusCode(OK.getStatusCode());
108-
}
109-
110-
@Test
111-
public void testAcceptRelationshipAnnouncementMessage() {
112-
// Create a COAR Notify Relationship Announcement message
113-
String citingResourceId = "https://doi.org/10.1234/example-publication";
114-
String relationship = DATACITE_URI_PREFIX + "Cites";
115-
116-
String message = createRelationshipAnnouncementMessage(citingResourceId, datasetPid, relationship);
117-
118-
// Send the message to the LDN inbox
119-
Response response = UtilIT.sendMessageToLDNInbox(message);
120-
121-
// Verify the response
122-
response.then().assertThat().statusCode(OK.getStatusCode()).body("data.message", equalTo("Message Received"));
123-
124-
// Wait a moment for notification to be created
125-
try {
126-
Thread.sleep(1000);
127-
} catch (InterruptedException e) {
128-
Thread.currentThread().interrupt();
129-
}
130-
131-
// Verify that a notification was created for the superuser
132-
Response notifications = UtilIT.getNotifications(superuserApiToken);
133-
134-
notifications.then().assertThat().statusCode(OK.getStatusCode()).body("data", notNullValue());
135-
136-
// Check that at least one notification exists with type DATASETMENTIONED
137-
String notificationsJson = notifications.asString();
138-
assertTrue(notificationsJson.contains("DATASETMENTIONED"), "Expected to find DATASETMENTIONED notification");
139-
assertTrue(notificationsJson.contains(citingResourceId), "Expected notification to contain citing resource ID");
140-
}
11+
private static String datasetPid ="doi:10.5072/F2ABCDEF";
14112

13+
//Simple test to assure that the default with no hosts allowed causes a message to get rejected.
14214
@Test
143-
@JvmSetting(key = JvmSettings.LINKEDDATANOTIFICATION_ALLOWED_HOSTS, value = "192.0.2.1")
144-
public void testRejectMessageFromNonWhitelistedHost() {
145-
String message = createRelationshipAnnouncementMessage("https://doi.org/10.1234/test", datasetPid,
15+
public void testRejectMessageFromNonAllowedHost() {
16+
String message = LDNInboxTest.createRelationshipAnnouncementMessage("https://doi.org/10.1234/test", datasetPid,
14617
DATACITE_URI_PREFIX + "Cites");
14718

14819
// Send the message - should be rejected
14920
Response response = UtilIT.sendMessageToLDNInbox(message);
15021

15122
response.then().assertThat().statusCode(FORBIDDEN.getStatusCode());
15223
}
153-
154-
@Test
155-
public void testRejectInvalidJsonLD() {
156-
String invalidJson = "{ this is not valid json }";
157-
158-
Response response = UtilIT.sendMessageToLDNInbox(invalidJson);
159-
160-
response.then().assertThat().statusCode(BAD_REQUEST.getStatusCode());
161-
}
162-
163-
@Test
164-
public void testRejectMessageForNonExistentDataset() {
165-
String nonExistentPid = "doi:10.5072/FK2/NONEXISTENT";
166-
167-
String message = createRelationshipAnnouncementMessage("https://doi.org/10.1234/test", nonExistentPid,
168-
DATACITE_URI_PREFIX + "Cites");
169-
170-
Response response = UtilIT.sendMessageToLDNInbox(message);
171-
172-
response.then().assertThat().statusCode(SERVICE_UNAVAILABLE.getStatusCode());
173-
}
174-
175-
@Test
176-
public void testAcceptMessageWithMultipleRelationshipTypes() {
177-
// Test different relationship types - the ones supported by Dataverse and the
178-
// default from DSpace
179-
String[] relationships = { DATACITE_URI_PREFIX + "Cites", DATACITE_URI_PREFIX + "IsSupplementTo",
180-
DATACITE_URI_PREFIX + "IsReferencedBy", DATACITE_URI_PREFIX + "IsCitedBy",
181-
DATACITE_URI_PREFIX + "IsSupplementedBy", DATACITE_URI_PREFIX + "References", FRBR_SUPPLEMENT };
182-
183-
for (String relationship : relationships) {
184-
String message = createRelationshipAnnouncementMessage(
185-
"https://doi.org/10.1234/test-" + relationship.toLowerCase(), datasetPid, relationship);
186-
187-
Response response = UtilIT.sendMessageToLDNInbox(message);
188-
189-
response.then().assertThat().statusCode(OK.getStatusCode()).body("data.message",
190-
equalTo("Message Received"));
191-
}
192-
}
193-
194-
@Test
195-
public void testAcceptMessageWithUrn() {
196-
// Test with URN format identifier
197-
String urnPid = "urn:nbn:de:0000-12345";
198-
199-
// Create a dataset with URN (in real scenario, this would be configured)
200-
// For this test, we'll use the existing dataset but reference it with a URN in
201-
// the message
202-
String message = createRelationshipAnnouncementMessage("https://doi.org/10.1234/test-urn", urnPid,
203-
DATACITE_URI_PREFIX + "Cites");
204-
205-
Response response = UtilIT.sendMessageToLDNInbox(message);
206-
207-
response.then().assertThat().statusCode(OK.getStatusCode());
208-
}
209-
210-
@Test
211-
public void testAcceptMessageWithHandle() {
212-
// Test with Handle format identifier
213-
String handlePid = "hdl:1234.5/67890";
214-
215-
String message = createRelationshipAnnouncementMessage("https://doi.org/10.1234/test-handle", handlePid,
216-
DATACITE_URI_PREFIX + "Cites");
217-
218-
Response response = UtilIT.sendMessageToLDNInbox(message);
219-
220-
response.then().assertThat().statusCode(OK.getStatusCode());
221-
}
222-
223-
@Test
224-
public void testRejectMessageWithNonUriRelationship() {
225-
// Test with an unsupported relationship type
226-
String message = createRelationshipAnnouncementMessage("https://doi.org/10.1234/test-unsupported", datasetPid,
227-
"UnsupportedRelationType");
228-
229-
Response response = UtilIT.sendMessageToLDNInbox(message);
230-
231-
// Should be rejected
232-
response.then().assertThat().statusCode(BAD_REQUEST.getStatusCode());
233-
}
234-
235-
/**
236-
* Helper method to create a COAR Notify Relationship Announcement message with
237-
* specific resource type
238-
*/
239-
private String createRelationshipAnnouncementMessage(String citingResourceId, String targetDatasetPid,
240-
String relationship) {
241-
242-
JsonObjectBuilder targetBuilder = Json.createObjectBuilder().add("id", RestAssured.baseURI)
243-
.add("inbox", RestAssured.baseURI + "/api/inbox").add("type", "Service");
244-
245-
JsonObject rel = Json.createObjectBuilder().add("as:object", targetDatasetPid)
246-
.add("as:relationship", relationship).add("as:subject", citingResourceId)
247-
.add("id", "urn:uuid:" + UUID.randomUUID().toString()).add("type", "Relationship").build();
248-
249-
return COARNotifyRelationshipAnnouncementStep.buildAnnouncement(rel, targetBuilder.build());
250-
}
25124
}

0 commit comments

Comments
 (0)