diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 227495196cf..801478d2980 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -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 @@ -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:: @@ -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" } ] } @@ -1612,7 +1617,7 @@ 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 @@ -1620,12 +1625,35 @@ 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 diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java index 93f4aca13d1..d0ba86ab68e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetLock.java @@ -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 { diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java index 8ebdc4745e6..f5a4acdffb8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetServiceBean.java @@ -452,13 +452,7 @@ public boolean checkDatasetLock(Long datasetId) { public List getDatasetLocksByUser( AuthenticatedUser user) { - TypedQuery 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) @@ -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 listLocks(DatasetLock.Reason lockType, AuthenticatedUser user) { + + TypedQuery 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 diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java index 0d8f60119db..dfdce43e895 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Datasets.java @@ -2517,7 +2517,7 @@ public Command 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 { @@ -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 locks = datasetService.getDatasetLocksByType(lockType); + List 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) { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 39c84562a09..ccde79bf233 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -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.*; @@ -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 ) { diff --git a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java index c08a71eea65..23c17c071ff 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/DatasetsIT.java @@ -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); @@ -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> 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(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 5ccccd8ec08..e9623599f51 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -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)