Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* Implement DELETE `/loan-storage/loans` by CQL query (CIRCSTORE-591)
* Set queue level based on TLR feature status (CIRCSTORE-589)
* Add `isRawHtml` to staff-slip schema and bump `staff-slips-storage` to 1.1 (CIRCSTORE-585)
* Allow anonymization of a list of requests (CIRCSTORE-647)

## 17.4.0 2025-03-12
* Upgrade to Java v21 (CIRCSTORE-576)
Expand Down
124 changes: 124 additions & 0 deletions src/main/java/org/folio/rest/impl/AnonymizeStorageRequestsAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.folio.rest.impl;

import static io.vertx.core.Future.succeededFuture;
import static org.folio.support.ModuleConstants.MODULE_NAME;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.ws.rs.core.Response;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.folio.rest.annotations.Validate;
import org.folio.rest.jaxrs.model.AnonymizeStorageRequestsRequest;
import org.folio.rest.jaxrs.model.AnonymizeStorageRequestsResponse;
import org.folio.rest.jaxrs.model.Errors;
import org.folio.rest.jaxrs.model.NotAnonymizedRequest;
import org.folio.rest.jaxrs.resource.AnonymizeStorageRequests;
import org.folio.rest.persist.PgUtil;
import org.folio.rest.persist.PostgresClient;
import org.folio.rest.tools.utils.TenantTool;
import org.folio.rest.tools.utils.ValidationHelper;
import org.folio.support.UUIDValidation;

import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.RowSet;
import jakarta.validation.constraints.NotNull;

public class AnonymizeStorageRequestsAPI implements AnonymizeStorageRequests {
private static final Logger log = LogManager.getLogger();

@Validate
@Override
public void postAnonymizeStorageRequests(AnonymizeStorageRequestsRequest request,
Map<String, String> okapiHeaders,
Handler<AsyncResult<Response>> responseHandler, Context vertxContext) {

AnonymizeStorageRequestsResponse response = new AnonymizeStorageRequestsResponse();
List<String> requestIds = request.getRequestIds();

Map<Boolean, List<String>> requestIdsMap = requestIds.stream()
.collect(Collectors.groupingBy(UUIDValidation::isValidUUID));

List<String> validIds = requestIdsMap.get(true);
List<String> invalidIds = requestIdsMap.get(false);

if (CollectionUtils.isNotEmpty(invalidIds)) {
log.warn("postAnonymizeStorageRequests:: Invalid request UUIDs provided: {}", invalidIds);
addToNotAnonymizedRequests(response, "invalidRequestIds", invalidIds);
}

if (CollectionUtils.isEmpty(validIds)) {
final Errors errors = ValidationHelper.createValidationErrorMessage(
"requestIds", requestIds.toString(), "Please provide valid requestIds");
responseHandler.handle(succeededFuture(
PostAnonymizeStorageRequestsResponse.respond422WithApplicationJson(errors)));
return;
}

log.info("postAnonymizeStorageRequests:: Anonymizing requests: {}", validIds.size());

final String tenantId = TenantTool.tenantId(okapiHeaders);
final PostgresClient postgresClient = PgUtil.postgresClient(vertxContext,
okapiHeaders);

final String combinedAnonymizationSql = createAnonymizationSQL(validIds,
tenantId);

executeSql(postgresClient, combinedAnonymizationSql).map(
updateResult -> PostAnonymizeStorageRequestsResponse.respond200WithApplicationJson(
response.withAnonymizedRequests(validIds)))
.map(Response.class::cast)
.otherwise(
e -> PostAnonymizeStorageRequestsResponse.respond500WithTextPlain(e.getMessage()))
.onComplete(responseHandler);

}

private void addToNotAnonymizedRequests(AnonymizeStorageRequestsResponse response,
String reason, List<String> ids) {
List<NotAnonymizedRequest> notAnonymizedRequests = response.getNotAnonymizedRequests();
notAnonymizedRequests.add(
new NotAnonymizedRequest().withReason(reason).withRequestIds(ids));
}

private Future<RowSet<Row>> executeSql(PostgresClient postgresClient, String sql) {
final Promise<RowSet<Row>> promise = Promise.promise();

postgresClient.execute(sql, promise::handle);

return promise.future();
}

private String createAnonymizationSQL(@NotNull Collection<String> requestIdList,
String tenantId) {

String requestIds = requestIdList.stream()
.map(s -> "\'" + s + "\'")
.collect(Collectors.joining(",", "(", ")"));

final String AnonymizeStorageRequestsSql = String.format(
new StringBuilder().append("UPDATE %s_%s.request ")
.append(" SET jsonb = jsonb - ARRAY['requesterId', 'proxyUserId', 'requester', 'proxy']")
.append(" WHERE request.id in ")
.append(requestIds)
.append(" AND request.jsonb->>'status' LIKE 'Closed - %%'")
.append(" AND (request.jsonb->>'requesterId' is NOT null")
.append(" OR request.jsonb->>'proxyUserId' is NOT null")
.append(" OR request.jsonb->>'requester' is NOT null")
.append(" OR request.jsonb->>'proxy' is NOT null)")
.toString(),
tenantId, MODULE_NAME);

Check warning on line 120 in src/main/java/org/folio/rest/impl/AnonymizeStorageRequestsAPI.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Immediately return this expression instead of assigning it to the temporary variable "AnonymizeStorageRequestsSql".

See more on https://sonarcloud.io/project/issues?id=org.folio%3Amod-circulation-storage&issues=AZ-tFMtE_3mVfk5XVBhK&open=AZ-tFMtE_3mVfk5XVBhK&pullRequest=586

return AnonymizeStorageRequestsSql;
}
}
165 changes: 165 additions & 0 deletions src/test/java/org/folio/rest/api/AnonymizeRequestsApiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package org.folio.rest.api;

import static org.folio.rest.api.StorageTestSuite.TENANT_ID;
import static org.folio.rest.support.ResponseHandler.json;
import static org.folio.rest.support.http.InterfaceUrls.anonymizeRequestsURL;
import static org.folio.rest.support.matchers.RequestMatchers.isAnonymized;
import static org.folio.rest.support.matchers.RequestMatchers.isNotAnonymized;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.core.Is.is;

import java.net.MalformedURLException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.folio.rest.jaxrs.model.AnonymizeStorageRequestsResponse;
import org.folio.rest.support.ApiTests;
import org.folio.rest.support.JsonResponse;
import org.folio.rest.support.builders.RequestRequestBuilder;
import org.folio.rest.support.http.AssertingRecordClient;
import org.folio.rest.support.http.InterfaceUrls;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

class AnonymizeRequestsApiTest extends ApiTests {
private final AssertingRecordClient requestsClient = new AssertingRecordClient(
client, TENANT_ID, InterfaceUrls::requestStorageUrl, "requests");

private final String firstRequestId = UUID.randomUUID().toString();
private final String secondRequestId = UUID.randomUUID().toString();

@BeforeEach
void beforeEach() throws MalformedURLException, InterruptedException,
ExecutionException, TimeoutException {

StorageTestSuite.deleteAll(InterfaceUrls.requestStorageUrl());

JsonObject request1 = requestsClient.create(new RequestRequestBuilder()
.hold()
.toHoldShelf()
.withId(UUID.fromString(firstRequestId))
.withItemId(UUID.randomUUID())
.withRequesterId(UUID.randomUUID())
.withRequester("Jones", "Stuart", "Anthony", "6837502674015")
.create()).getJson();

JsonObject request2 = requestsClient.create(new RequestRequestBuilder()
.hold()
.toHoldShelf()
.withId(UUID.fromString(secondRequestId))
.withItemId(UUID.randomUUID())
.withRequesterId(UUID.randomUUID())
.withRequester("Jones", "Stuart", "Anthony", "6837502674015")
.withProxyId(UUID.randomUUID())
.withProxy("Stuart", "Rebecca", "6059539205")
.create()).getJson();

requestsClient.replace(firstRequestId, RequestRequestBuilder.from(request1).closed());
requestsClient.replace(secondRequestId, RequestRequestBuilder.from(request2).closed());
}

@AfterEach
void checkIdsAfterEach() {
StorageTestSuite.checkForMismatchedIDs("request");
}

@Test
void canAnonymizeRequests() throws InterruptedException, ExecutionException,
TimeoutException, MalformedURLException {

final var response = anonymizeRequests(firstRequestId, secondRequestId);

assertThat(response.getAnonymizedRequests(), containsInAnyOrder(firstRequestId, secondRequestId));
assertThat(requestsClient.getById(firstRequestId).getJson(), isAnonymized());
assertThat(requestsClient.getById(secondRequestId).getJson(), isAnonymized());
}

@Test
void canAnonymizeAlreadyAnonymizedRequests() throws InterruptedException, ExecutionException,
TimeoutException, MalformedURLException {

var response = anonymizeRequests(firstRequestId, secondRequestId);

assertThat(response.getAnonymizedRequests(), containsInAnyOrder(firstRequestId, secondRequestId));
assertThat(requestsClient.getById(firstRequestId).getJson(), isAnonymized());
assertThat(requestsClient.getById(secondRequestId).getJson(), isAnonymized());

response = anonymizeRequests(firstRequestId, secondRequestId);

assertThat(response.getAnonymizedRequests(), containsInAnyOrder(firstRequestId, secondRequestId));
assertThat(requestsClient.getById(firstRequestId).getJson(), isAnonymized());
assertThat(requestsClient.getById(secondRequestId).getJson(), isAnonymized());
}

@Test
void onlyAnonymizesOpenRequests() throws InterruptedException, ExecutionException,
TimeoutException, MalformedURLException {

final var openRequestId = UUID.randomUUID().toString();

requestsClient.create(new RequestRequestBuilder()
.hold()
.toHoldShelf()
.withId(UUID.fromString(openRequestId))
.withItemId(UUID.randomUUID())
.withRequesterId(UUID.randomUUID())
.withRequester("Jones", "Stuart", "Anthony", "6837502674015")
.create()).getJson();

final var response = anonymizeRequests(firstRequestId, secondRequestId, openRequestId);

// This implementation is carried over from anonymizing loans
// The id is returned as anonymized but actually won't be unless criteria is met
assertThat(response.getAnonymizedRequests(), containsInAnyOrder(firstRequestId, secondRequestId, openRequestId));
assertThat(requestsClient.getById(openRequestId).getJson(), isNotAnonymized());
}

@Test
void canNotAnonymizeEmptyList() throws MalformedURLException {
JsonResponse response = attemptAnonymizeRequests();

assertThat(response.getStatusCode(), is(422));
}

@Test
void canAnonymizeInvalidAndValidUuids() throws MalformedURLException {
final String firstNotValidId = "not valid";
final String secondNotValidId = "null";

final var response = anonymizeRequests(firstRequestId, secondRequestId,
firstNotValidId, secondNotValidId);

assertThat(response.getAnonymizedRequests(), containsInAnyOrder(firstRequestId, secondRequestId));
assertThat(response.getNotAnonymizedRequests().size(), is(1));
assertThat(response.getNotAnonymizedRequests().get(0).getReason(), is("invalidRequestIds"));
assertThat(response.getNotAnonymizedRequests().get(0).getRequestIds(),
containsInAnyOrder(firstNotValidId, secondNotValidId));
}

private AnonymizeStorageRequestsResponse anonymizeRequests(String... requestIds) throws MalformedURLException {
final JsonResponse response = attemptAnonymizeRequests(requestIds);

assertThat(response.getStatusCode(), is(200));

return response.getJson().mapTo(AnonymizeStorageRequestsResponse.class);
}

private JsonResponse attemptAnonymizeRequests(String... requestIds) throws MalformedURLException {
final var requestBody = new JsonObject()
.put("requestIds", new JsonArray(List.of(requestIds)));

final var completed = new CompletableFuture<JsonResponse>();

client.post(anonymizeRequestsURL(), requestBody, TENANT_ID, json(completed));

return get(completed);
}
}
Loading
Loading