-
Notifications
You must be signed in to change notification settings - Fork 28
CIRC-2292: Implement Request Anonymization API #1631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
3fde4ae
First push
longvo-cv faa563a
Merge remote-tracking branch 'origin/CIRC-2292' into feature/request-…
longvo-cv d67642e
typo and anonymization result
longvo-cv 35e0564
Added tests and update request files
longvo-cv 83ed124
Merge branch 'master' into CIRC-2292
yuntianhu d52d323
Fix linting and coverage
longvo-cv 59c76f5
Merge CIRC-2292 with lint and coverage fixes
longvo-cv 1495690
Add additional tests for coverage
longvo-cv 0f24896
Add additional test for coverage and fix comments
longvo-cv 9723b50
Merge branch 'master' into CIRC-2292
yuntianhu ea37184
Integration testing with API tests
longvo-cv 4ab3b88
Merge branch 'CIRC-2292' of https://github.com/folio-org/mod-circulat…
longvo-cv b64a513
Update integration test to work without mod-circulation-storage
longvo-cv 16ef698
Change endpoint path to /request-anonymization
longvo-cv c34a9b5
Fix minor API test
longvo-cv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "processed": 2, | ||
| "anonymizedRequests": [ | ||
| "cf23adf0-61ba-4887-bf82-956c4aae2260", | ||
| "550e8400-e29b-41d4-a716-446655440000" | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "requestIds": [ | ||
| "cf23adf0-61ba-4887-bf82-956c4aae2260", | ||
| "550e8400-e29b-41d4-a716-446655440000" | ||
| ], | ||
| "includeCirculationLogs": true | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-04/schema#", | ||
| "description": "Result of request anonymization operation containing the number of processed requests and list of anonymized request IDs", | ||
| "type": "object", | ||
| "properties": { | ||
| "processed": { | ||
| "type": "integer", | ||
| "description": "Total number of requests processed in the anonymization operation" | ||
| }, | ||
| "anonymizedRequests": { | ||
| "type": "array", | ||
| "description": "List of UUIDs for requests that were successfully anonymized", | ||
| "items": { | ||
| "type": "string", | ||
| "format": "uuid", | ||
| "description": "UUID of an anonymized request" | ||
| } | ||
| } | ||
| }, | ||
| "required": ["processed", "anonymizedRequests"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-04/schema#", | ||
| "description": "Request to anonymize one or more closed circulation requests by removing personally identifiable information", | ||
| "type": "object", | ||
| "properties": { | ||
| "requestIds": { | ||
| "type": "array", | ||
| "description": "Array of request UUIDs to be anonymized. All requests must have a closed status.", | ||
| "items": { | ||
| "type": "string", | ||
| "format": "uuid", | ||
| "description": "UUID of a request to anonymize" | ||
| } | ||
| }, | ||
| "includeCirculationLogs": { | ||
| "type": "boolean", | ||
| "description": "Whether to anonymize circulation logs associated with these requests. When true, existing circulation log entries will have userBarcode set to '-' and a new anonymization log entry will be created. Defaults to true if not specified.", | ||
| "default": true | ||
| } | ||
| }, | ||
| "required": ["requestIds"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
src/main/java/org/folio/circulation/resources/RequestAnonymizationResource.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| package org.folio.circulation.resources; | ||
|
|
||
| import static org.folio.circulation.support.results.Result.succeeded; | ||
|
|
||
| import java.util.List; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import org.folio.circulation.infrastructure.storage.requests.RequestRepository; | ||
| import org.folio.circulation.support.Clients; | ||
| import org.folio.circulation.support.RouteRegistration; | ||
| import org.folio.circulation.support.http.server.JsonHttpResponse; | ||
| import org.folio.circulation.support.http.server.ValidationError; | ||
| import org.folio.circulation.support.http.server.WebContext; | ||
| import org.folio.circulation.support.ValidationErrorFailure; | ||
| import org.folio.circulation.support.results.Result; | ||
| import org.folio.circulation.domain.RequestStatus; | ||
| import org.folio.circulation.support.logging.Logging; | ||
|
|
||
| import io.vertx.core.http.HttpClient; | ||
| import io.vertx.core.json.JsonArray; | ||
| import io.vertx.core.json.JsonObject; | ||
| import io.vertx.ext.web.Router; | ||
| import io.vertx.ext.web.RoutingContext; | ||
|
|
||
| public class RequestAnonymizationResource extends Resource { | ||
|
|
||
| private static final java.lang.invoke.MethodHandles.Lookup LOOKUP = | ||
| java.lang.invoke.MethodHandles.lookup(); | ||
| private static final org.apache.logging.log4j.Logger log = | ||
| org.apache.logging.log4j.LogManager.getLogger(LOOKUP.lookupClass()); | ||
| public RequestAnonymizationResource(HttpClient client) { | ||
| super(client); | ||
| } | ||
|
|
||
| @Override | ||
| public void register(Router router) { | ||
| RouteRegistration routeRegistration = new RouteRegistration( | ||
| "/request-anonymization", router); | ||
| routeRegistration.create(this::anonymizeRequests); | ||
| } | ||
|
|
||
| void anonymizeRequests(RoutingContext routingContext) { | ||
| final WebContext context = new WebContext(routingContext); | ||
| final Clients clients = Clients.create(context, client); | ||
|
|
||
| JsonObject body = routingContext.getBodyAsJson(); | ||
|
|
||
| // Validate request body | ||
| if (body == null || !body.containsKey("requestIds")) { | ||
| log.warn("anonymizeRequests:: Request body missing requestIds"); | ||
| Result<JsonObject> failedResult = Result.failed(new ValidationErrorFailure( | ||
| new ValidationError("requestIds array is required", "requestIds", null))); | ||
| context.writeResultToHttpResponse(failedResult.map(JsonHttpResponse::ok)); | ||
| return; | ||
| } | ||
|
|
||
| List<String> requestIds = body.getJsonArray("requestIds") | ||
| .stream() | ||
| .map(Object::toString) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| if (requestIds.isEmpty()) { | ||
| log.warn("anonymizeRequests:: requestIds array is empty"); | ||
| Result<JsonObject> failedResult = Result.failed(new ValidationErrorFailure( | ||
| new ValidationError("requestIds array cannot be empty", "requestIds", null))); | ||
| context.writeResultToHttpResponse(failedResult.map(JsonHttpResponse::ok)); | ||
| return; | ||
| } | ||
|
|
||
| // Get includeCirculationLogs parameter (default to true) | ||
| boolean includeCirculationLogs = body.getBoolean("includeCirculationLogs", true); | ||
| log.info("anonymizeRequests:: Processing {} requests, includeCirculationLogs={}", | ||
| requestIds.size(), includeCirculationLogs); | ||
|
|
||
| // Chain the operations | ||
| validateRequestsEligible(requestIds, clients) | ||
| .thenCompose(r -> r.after(v -> | ||
| anonymizeRequestsInStorage(requestIds, includeCirculationLogs, clients))) | ||
| .thenApply(r -> r.map(v -> { | ||
| log.info("anonymizeRequests:: Successfully anonymized {} requests", requestIds.size()); | ||
| return new JsonObject() | ||
| .put("processed", requestIds.size()) | ||
| .put("anonymizedRequests", new JsonArray(requestIds)); | ||
| })) | ||
| .thenApply(r -> r.map(JsonHttpResponse::ok)) | ||
| .thenAccept(result -> { | ||
| if (result.failed()) { | ||
| log.error("anonymizeRequests:: Failed to anonymize requests: {}", | ||
| result.cause().toString()); | ||
| } | ||
| context.writeResultToHttpResponse(result); | ||
| }); | ||
| } | ||
|
|
||
| CompletableFuture<Result<Void>> validateRequestsEligible( | ||
| List<String> requestIds, Clients clients) { | ||
|
|
||
| RequestRepository requestRepository = new RequestRepository(clients); | ||
|
|
||
| List<CompletableFuture<Result<org.folio.circulation.domain.Request>>> futures = | ||
| requestIds.stream() | ||
| .map(requestRepository::getById) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) | ||
| .thenApply(v -> { | ||
| for (CompletableFuture<Result<org.folio.circulation.domain.Request>> future : futures) { | ||
| Result<org.folio.circulation.domain.Request> result = future.join(); | ||
|
|
||
| if (result.failed()) { | ||
| log.warn("validateRequestsEligible:: Failed to retrieve request: {}", | ||
| result.cause().toString()); | ||
| return Result.failed(result.cause()); | ||
| } | ||
|
|
||
| org.folio.circulation.domain.Request request = result.value(); | ||
| RequestStatus status = request.getStatus(); | ||
|
|
||
| // Use the RequestStatus enum for cleaner validation | ||
| boolean isEligible = status == RequestStatus.CLOSED_FILLED || | ||
| status == RequestStatus.CLOSED_CANCELLED || | ||
| status == RequestStatus.CLOSED_PICKUP_EXPIRED || | ||
| status == RequestStatus.CLOSED_UNFILLED; | ||
|
|
||
| if (!isEligible) { | ||
| log.warn("validateRequestsEligible:: Request {} has ineligible status: {}", | ||
| request.getId(), status.getValue()); | ||
| return Result.failed(new ValidationErrorFailure( | ||
| new ValidationError( | ||
| "Request " + request.getId() + " cannot be anonymized - status must be closed", | ||
| "status", status.getValue()))); | ||
| } | ||
|
|
||
| // Optional: Check if already anonymized | ||
| if (request.getRequesterId() == null || request.getRequesterId().isEmpty()) { | ||
| log.warn("validateRequestsEligible:: Request {} appears to be already anonymized", | ||
| request.getId()); | ||
| return Result.failed(new ValidationErrorFailure( | ||
| new ValidationError( | ||
| "Request " + request.getId() + " appears to be already anonymized", | ||
| "requesterId", null))); | ||
| } | ||
| } | ||
|
|
||
| log.info("validateRequestsEligible:: All {} requests are eligible for anonymization", | ||
| requestIds.size()); | ||
| return succeeded(null); | ||
| }); | ||
| } | ||
|
|
||
| CompletableFuture<Result<Void>> anonymizeRequestsInStorage( | ||
| List<String> requestIds, boolean includeCirculationLogs, Clients clients) { | ||
|
|
||
| JsonObject payload = new JsonObject() | ||
| .put("requestIds", new JsonArray(requestIds)) | ||
| .put("includeCirculationLogs", includeCirculationLogs); | ||
|
|
||
| log.info("anonymizeRequestsInStorage:: Sending anonymization request to storage layer"); | ||
|
|
||
| return clients.requestsStorage() | ||
| .post(payload, "/request-storage/requests/anonymize") | ||
| .thenApply(r -> { | ||
| if (r.succeeded()) { | ||
| log.info("anonymizeRequestsInStorage:: Storage layer successfully processed requests"); | ||
| } else { | ||
| log.error("anonymizeRequestsInStorage:: Storage layer failed: {}", | ||
| r.cause().toString()); | ||
| } | ||
| return r.map(response -> null); | ||
| }); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add top level description, please. thank you