Skip to content

Commit

Permalink
Merge pull request #8445 from IQSS/local135
Browse files Browse the repository at this point in the history
Additions to the Locks API
  • Loading branch information
kcondon authored Feb 24, 2022
2 parents f32ce08 + 0905ad3 commit ee7cc4b
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 17 deletions.
38 changes: 33 additions & 5 deletions doc/sphinx-guides/source/api/native-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,9 @@ The fully expanded example above (without environment variables) looks like this
Dataset Locks
~~~~~~~~~~~~~
Manage Locks on a Specific Dataset
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To check if a dataset is locked:
.. code-block:: bash
Expand Down Expand Up @@ -1551,7 +1554,7 @@ The fully expanded example above (without environment variables) looks like this
curl "https://demo.dataverse.org/api/datasets/24/locks?type=Ingest"
Currently implemented lock types are ``Ingest``, ``Workflow``, ``InReview``, ``DcmUpload``, ``pidRegister``, and ``EditInProgress``.
Currently implemented lock types are ``Ingest``, ``Workflow``, ``InReview``, ``DcmUpload``, ``finalizePublication``, ``EditInProgress`` and ``FileValidationFailed``.
The API will output the list of locks, for example::
Expand All @@ -1560,12 +1563,14 @@ The API will output the list of locks, for example::
{
"lockType":"Ingest",
"date":"Fri Aug 17 15:05:51 EDT 2018",
"user":"dataverseAdmin"
"user":"dataverseAdmin",
"dataset":"doi:12.34567/FK2/ABCDEF"
},
{
"lockType":"Workflow",
"date":"Fri Aug 17 15:02:00 EDT 2018",
"user":"dataverseAdmin"
"user":"dataverseAdmin",
"dataset":"doi:12.34567/FK2/ABCDEF"
}
]
}
Expand Down Expand Up @@ -1612,20 +1617,43 @@ Or, to delete a lock of the type specified only. Note that this requires “supe
export API_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export SERVER_URL=https://demo.dataverse.org
export ID=24
export LOCK_TYPE=pidRegister
export LOCK_TYPE=finalizePublication
curl -H "X-Dataverse-key: $API_TOKEN" -X DELETE $SERVER_URL/api/datasets/$ID/locks?type=$LOCK_TYPE
The fully expanded example above (without environment variables) looks like this:
.. code-block:: bash
curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE https://demo.dataverse.org/api/datasets/24/locks?type=pidRegister
curl -H "X-Dataverse-key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -X DELETE https://demo.dataverse.org/api/datasets/24/locks?type=finalizePublication
If the dataset is not locked (or if there is no lock of the specified type), the API will exit with a warning message.
(Note that the API calls above all support both the database id and persistent identifier notation for referencing the dataset)
List Locks Across All Datasets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Note that this API requires “superuser” credentials. You must supply the ``X-Dataverse-key`` header with the api token of an admin user (as in the example below).
The output of this API is formatted identically to the API that lists the locks for a specific dataset, as in one of the examples above.
Use the following API to list ALL the locks on all the datasets in your installation:
``/api/datasets/locks``
The listing can be filtered by specific lock type **and/or** user, using the following *optional* query parameters:
* ``userIdentifier`` - To list the locks owned by a specific user
* ``type`` - To list the locks of the type specified. If the supplied value does not match a known lock type, the API will return an error and a list of valid lock types. As of writing this, the implemented lock types are ``Ingest``, ``Workflow``, ``InReview``, ``DcmUpload``, ``finalizePublication``, ``EditInProgress`` and ``FileValidationFailed``.
For example:
.. code-block:: bash
curl -H "X-Dataverse-key: xxx" "http://localhost:8080/api/datasets/locks?type=Ingest&userIdentifier=davis4ever"
.. _dataset-metrics-api:
Dataset Metrics
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/DatasetLock.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,16 @@
@Entity
@Table(indexes = {@Index(columnList="user_id"), @Index(columnList="dataset_id")})
@NamedQueries({
@NamedQuery(name = "DatasetLock.findAll",
query="SELECT lock FROM DatasetLock lock ORDER BY lock.id"),
@NamedQuery(name = "DatasetLock.getLocksByDatasetId",
query = "SELECT lock FROM DatasetLock lock WHERE lock.dataset.id=:datasetId"),
@NamedQuery(name = "DatasetLock.getLocksByAuthenticatedUserId",
query = "SELECT lock FROM DatasetLock lock WHERE lock.user.id=:authenticatedUserId")
@NamedQuery(name = "DatasetLock.getLocksByType",
query = "SELECT lock FROM DatasetLock lock WHERE lock.reason=:lockType ORDER BY lock.id"),
@NamedQuery(name = "DatasetLock.getLocksByAuthenticatedUserId",
query = "SELECT lock FROM DatasetLock lock WHERE lock.user.id=:authenticatedUserId ORDER BY lock.id"),
@NamedQuery(name = "DatasetLock.getLocksByTypeAndAuthenticatedUserId",
query = "SELECT lock FROM DatasetLock lock WHERE lock.reason=:lockType AND lock.user.id=:authenticatedUserId ORDER BY lock.id")
}
)
public class DatasetLock implements Serializable {
Expand Down
38 changes: 31 additions & 7 deletions src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -452,13 +452,7 @@ public boolean checkDatasetLock(Long datasetId) {

public List<DatasetLock> getDatasetLocksByUser( AuthenticatedUser user) {

TypedQuery<DatasetLock> query = em.createNamedQuery("DatasetLock.getLocksByAuthenticatedUserId", DatasetLock.class);
query.setParameter("authenticatedUserId", user.getId());
try {
return query.getResultList();
} catch (javax.persistence.NoResultException e) {
return null;
}
return listLocks(null, user);
}

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
Expand Down Expand Up @@ -533,6 +527,36 @@ public void updateDatasetLock(DatasetLock datasetLock) {
em.merge(datasetLock);
}

/*
* Lists all dataset locks, optionally filtered by lock type or user, or both
* @param lockType
* @param user
* @return a list of DatasetLocks
*/
public List<DatasetLock> listLocks(DatasetLock.Reason lockType, AuthenticatedUser user) {

TypedQuery<DatasetLock> query;

if (lockType == null && user == null) {
query = em.createNamedQuery("DatasetLock.findAll", DatasetLock.class);
} else if (user == null) {
query = em.createNamedQuery("DatasetLock.getLocksByType", DatasetLock.class);
query.setParameter("lockType", lockType);
} else if (lockType == null) {
query = em.createNamedQuery("DatasetLock.getLocksByAuthenticatedUserId", DatasetLock.class);
query.setParameter("authenticatedUserId", user.getId());
} else {
query = em.createNamedQuery("DatasetLock.getLocksByTypeAndAuthenticatedUserId", DatasetLock.class);
query.setParameter("lockType", lockType);
query.setParameter("authenticatedUserId", user.getId());
}
try {
return query.getResultList();
} catch (javax.persistence.NoResultException e) {
return null;
}
}

/*
getTitleFromLatestVersion methods use native query to return a dataset title
Expand Down
56 changes: 55 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -2517,7 +2517,7 @@ public Command<DatasetVersion> handleLatestPublished() {

@GET
@Path("{identifier}/locks")
public Response getLocks(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) {
public Response getLocksForDataset(@PathParam("identifier") String id, @QueryParam("type") DatasetLock.Reason lockType) {

Dataset dataset = null;
try {
Expand Down Expand Up @@ -2642,6 +2642,60 @@ public Response lockDataset(@PathParam("identifier") String id, @PathParam("type
});
}

@GET
@Path("locks")
public Response listLocks(@QueryParam("type") String lockType, @QueryParam("userIdentifier") String userIdentifier) { //DatasetLock.Reason lockType) {
// This API is here, under /datasets, and not under /admin, because we
// likely want it to be accessible to admin users who may not necessarily
// have localhost access, that would be required to get to /api/admin in
// most installations. It is still reasonable however to limit access to
// this api to admin users only.
AuthenticatedUser apiUser;
try {
apiUser = findAuthenticatedUserOrDie();
} catch (WrappedResponse ex) {
return error(Response.Status.UNAUTHORIZED, "Authentication is required.");
}
if (!apiUser.isSuperuser()) {
return error(Response.Status.FORBIDDEN, "Superusers only.");
}

// Locks can be optinally filtered by type, user or both.
DatasetLock.Reason lockTypeValue = null;
AuthenticatedUser user = null;

// For the lock type, we use a QueryParam of type String, instead of
// DatasetLock.Reason; that would be less code to write, but this way
// we can check if the value passed matches a valid lock type ("reason")
// and provide a helpful error message if it doesn't. If you use a
// QueryParam of an Enum type, trying to pass an invalid value to it
// results in a potentially confusing "404/NOT FOUND - requested
// resource is not available".
if (lockType != null && !lockType.isEmpty()) {
try {
lockTypeValue = DatasetLock.Reason.valueOf(lockType);
} catch (IllegalArgumentException iax) {
String validValues = Strings.join(",", DatasetLock.Reason.values());
String errorMessage = "Invalid lock type value: " + lockType +
"; valid lock types: " + validValues;
return error(Response.Status.BAD_REQUEST, errorMessage);
}
}

if (userIdentifier != null && !userIdentifier.isEmpty()) {
user = authSvc.getAuthenticatedUser(userIdentifier);
if (user == null) {
return error(Response.Status.BAD_REQUEST, "Unknown user identifier: "+userIdentifier);
}
}

//List<DatasetLock> locks = datasetService.getDatasetLocksByType(lockType);
List<DatasetLock> locks = datasetService.listLocks(lockTypeValue, user);

return ok(locks.stream().map(lock -> json(lock)).collect(toJsonArray()));
}


@GET
@Path("{id}/makeDataCount/citations")
public Response getMakeDataCountCitations(@PathParam("id") String idSupplied) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.workflow.Workflow;
import edu.harvard.iq.dataverse.workflow.step.WorkflowStepData;
import java.math.BigDecimal;

import java.net.URISyntaxException;
import java.util.*;
Expand Down Expand Up @@ -135,8 +136,8 @@ public static JsonObjectBuilder json(DatasetLock lock) {
.add("lockType", lock.getReason().toString())
.add("date", lock.getStartTime().toString())
.add("user", lock.getUser().getUserIdentifier())
.add("dataset", lock.getDataset().getGlobalId().asString())
.add("message", lock.getInfo());

}

public static JsonObjectBuilder json( RoleAssigneeDisplayInfo d ) {
Expand Down
93 changes: 92 additions & 1 deletion src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,7 @@ public void testDatasetLocksApi() {
Response createDatasetResponse = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, apiToken);
createDatasetResponse.prettyPrint();
Integer datasetId = UtilIT.getDatasetIdFromResponse(createDatasetResponse);
String persistentIdentifier = UtilIT.getDatasetPersistentIdFromResponse(createDatasetResponse);

// This should return an empty list, as the dataset should have no locks just yet:
Response checkDatasetLocks = UtilIT.checkDatasetLocks(datasetId.longValue(), null, apiToken);
Expand Down Expand Up @@ -1850,7 +1851,97 @@ public void testDatasetLocksApi() {
lockDatasetResponse.then().assertThat()
.body("message", equalTo("dataset already locked with lock type Ingest"))
.statusCode(FORBIDDEN.getStatusCode());


// Let's also test the new (as of 5.10) API that lists the locks
// present across all datasets.

// First, we'll try listing ALL locks currently in the system, and make sure that the ingest lock
// for this dataset is on the list:
checkDatasetLocks = UtilIT.listAllLocks(apiToken);
checkDatasetLocks.prettyPrint();
checkDatasetLocks.then().assertThat()
.statusCode(200);

boolean lockListedCorrectly = false;
List<Map<String, String>> listedLockEntries = checkDatasetLocks.body().jsonPath().getList("data");
for (int i = 0; i < listedLockEntries.size(); i++) {
if ("Ingest".equals(listedLockEntries.get(i).get("lockType"))
&& username.equals(listedLockEntries.get(i).get("user"))
&& persistentIdentifier.equals(listedLockEntries.get(i).get("dataset"))) {
lockListedCorrectly = true;
break;
}
}
assertTrue("Lock missing from the output of /api/datasets/locks", lockListedCorrectly);

// Try the same, but with an api token of a random, non-super user
// (this should get rejected):
createUser = UtilIT.createRandomUser();
createUser.prettyPrint();
String wrongApiToken = UtilIT.getApiTokenFromResponse(createUser);
checkDatasetLocks = UtilIT.listAllLocks(wrongApiToken);
checkDatasetLocks.prettyPrint();
checkDatasetLocks.then().assertThat()
.statusCode(FORBIDDEN.getStatusCode());

// Try to narrow the listing down to the lock of type=Ingest specifically;
// verify that the lock in question is still being listed:
checkDatasetLocks = UtilIT.listLocksByType("Ingest", apiToken);
checkDatasetLocks.prettyPrint();
// We'll again assume that it's possible that the API is going to list
// *multiple* locks; i.e. that there are other datasets with the lock
// of type "Ingest" on them. So we'll go through the list and look for the
// lock for this specific dataset again.
lockListedCorrectly = false;
listedLockEntries = checkDatasetLocks.body().jsonPath().getList("data");
for (int i = 0; i < listedLockEntries.size(); i++) {
if ("Ingest".equals(listedLockEntries.get(i).get("lockType"))
&& username.equals(listedLockEntries.get(i).get("user"))
&& persistentIdentifier.equals(listedLockEntries.get(i).get("dataset"))) {
lockListedCorrectly = true;
break;
}
}
assertTrue("Lock missing from the output of /api/datasets/locks?type=Ingest", lockListedCorrectly);


// Try to list locks of an invalid type:
checkDatasetLocks = UtilIT.listLocksByType("BadLockType", apiToken);
checkDatasetLocks.prettyPrint();
checkDatasetLocks.then().assertThat()
.body("message", startsWith("Invalid lock type value: BadLockType"))
.statusCode(BAD_REQUEST.getStatusCode());

// List the locks owned by the current user; verify that the lock above
// is still listed:
checkDatasetLocks = UtilIT.listLocksByUser(username, apiToken);
checkDatasetLocks.prettyPrint();
// Safe to assume there should be only one:
checkDatasetLocks.then().assertThat()
.body("data[0].lockType", equalTo("Ingest"))
.body("data[0].user", equalTo(username))
.body("data[0].dataset", equalTo(persistentIdentifier))
.statusCode(200);

// Further narrow down the listing to both the type AND user:
checkDatasetLocks = UtilIT.listLocksByTypeAndUser("Ingest", username, apiToken);
checkDatasetLocks.prettyPrint();
// Even safer to assume there should be only one:
checkDatasetLocks.then().assertThat()
.statusCode(200)
.body("data[0].lockType", equalTo("Ingest"))
.body("data[0].user", equalTo(username))
.body("data[0].dataset", equalTo(persistentIdentifier));


// Finally, try asking for the locks owned by this user AND of type "InReview".
// This should produce an empty list:
checkDatasetLocks = UtilIT.listLocksByTypeAndUser("InReview", username, apiToken);
checkDatasetLocks.prettyPrint();
checkDatasetLocks.then().assertThat()
.statusCode(200)
.body("data", equalTo(emptyArray));

// And now test deleting the lock:
Response unlockDatasetResponse = UtilIT.unlockDataset(datasetId.longValue(), "Ingest", apiToken);
unlockDatasetResponse.prettyPrint();
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,34 @@ static Response checkDatasetLocks(String idOrPersistentId, String lockType, Stri
return response;
}

static Response listAllLocks(String apiToken) {
Response response = given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("api/datasets/locks");
return response;
}

static Response listLocksByType(String lockType, String apiToken) {
Response response = given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("api/datasets/locks?type="+lockType);
return response;
}

static Response listLocksByUser(String userIdentifier, String apiToken) {
Response response = given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("api/datasets/locks?userIdentifier="+userIdentifier);
return response;
}

static Response listLocksByTypeAndUser(String lockType, String userIdentifier, String apiToken) {
Response response = given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
.get("api/datasets/locks?type="+lockType+"&userIdentifier="+userIdentifier);
return response;
}

static Response lockDataset(long datasetId, String lockType, String apiToken) {
Response response = given()
.header(API_TOKEN_HTTP_HEADER, apiToken)
Expand Down

0 comments on commit ee7cc4b

Please sign in to comment.