diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java index 72014e26d..e34b2b785 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java @@ -45,6 +45,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -208,6 +209,59 @@ public Map> getPolicies(// .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); } + @GetMapping("/policies/attributes/{field}") + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasAuthority('" + IrsRoles.ADMIN_IRS + "')") + @Operation(summary = "Autocomplete for policy fields", + description = "Provides autocomplete suggestions for policy fields based on input criteria.", + tags = { "Policy" }, responses = { @ApiResponse(responseCode = "200", + description = "Successful retrieval of autocomplete suggestions", + content = @Content(mediaType = "application/json", + schema = @Schema( + implementation = List.class))), + @ApiResponse(responseCode = "400", + description = "Invalid input parameters", + content = { @Content(mediaType = APPLICATION_JSON_VALUE, + schema = @Schema( + implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "error", + ref = "#/components/examples/error-response-403")) + }), + @ApiResponse(responseCode = "401", description = UNAUTHORIZED_DESC, + content = { @Content(mediaType = APPLICATION_JSON_VALUE, + schema = @Schema( + implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "error", + ref = "#/components/examples/error-response-401")) + }), + @ApiResponse(responseCode = "403", description = FORBIDDEN_DESC, + content = { @Content(mediaType = APPLICATION_JSON_VALUE, + schema = @Schema( + implementation = ErrorResponse.class), + examples = @ExampleObject( + name = "error", + ref = "#/components/examples/error-response-403")) + }) + }, security = @SecurityRequirement(name = "bearerAuth")) + public List autocomplete( + @Parameter(description = "The field to autocomplete (BPN, policyId, createdOn, validUntil, action)") // + @PathVariable("field") final String field, + + @Parameter(description = "Search query with restricted character set") @Pattern( + regexp = "^[a-zA-Z0-9\\-\\+: ]*$", + message = "Parameter 's' contains invalid characters") @RequestParam("s") final String value, + + @Parameter(description = "Limit for the number of results, default is 10 and max is 100") @RequestParam( + name = "limit", required = false, defaultValue = "10") @Max(value = 100, + message = "Parameter 'limit' is above max") final int limit) { + + final Map> bpnToPoliciesMap = service.getPolicies(null); + return policyPagingService.autocomplete(bpnToPoliciesMap, field, value, limit); + + } + // TODO (mfischer): #639: add documentation and insomnia collection @GetMapping("/policies/paged") @ResponseStatus(HttpStatus.OK) diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/services/PolicyPagingService.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/services/PolicyPagingService.java index fa7d89a4d..fa6ef70f8 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/services/PolicyPagingService.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/services/PolicyPagingService.java @@ -27,10 +27,13 @@ import static org.eclipse.tractusx.irs.policystore.models.SearchCriteria.Operation.EQUALS; import static org.eclipse.tractusx.irs.policystore.models.SearchCriteria.Operation.STARTS_WITH; +import java.time.format.DateTimeFormatter; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -66,18 +69,9 @@ public Page getPolicies(final Map> bpnToPoli final Comparator comparator = new PolicyComparatorBuilder(pageable).build(); final Predicate filter = new PolicyFilterBuilder(searchCriteria).build(); - - final List policies = bpnToPoliciesMap.entrySet() - .stream() - .flatMap(bpnWithPolicies -> bpnWithPolicies.getValue() - .stream() - .map(policy -> new PolicyWithBpn( - bpnWithPolicies.getKey(), - policy))) - .filter(filter) - .sorted(comparator) - .toList(); - + final List policies = getPolicyWithBpnStream(bpnToPoliciesMap).filter(filter) + .sorted(comparator) + .toList(); return applyPaging(pageable, policies); } @@ -85,11 +79,61 @@ private PageImpl applyPaging(final Pageable pageable, final List< final int start = Math.min(pageable.getPageNumber() * pageable.getPageSize(), policies.size()); final int end = Math.min((pageable.getPageNumber() + 1) * pageable.getPageSize(), policies.size()); final List pagedPolicies = policies.subList(start, end); - return new PageImpl<>(pagedPolicies, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()), policies.size()); } + public List autocomplete(final Map> bpnToPoliciesMap, final String field, + final String value, final int limit) { + + if (PROPERTY_BPN.equalsIgnoreCase(field)) { + return bpnToPoliciesMap.keySet().stream().filter(t -> StringUtils.startsWithIgnoreCase(t, value)).toList(); + } else { + final Function fieldSelector = getFieldSelector(field); + final Stream policyWithBpnStream = getPolicyWithBpnStream(bpnToPoliciesMap); + return policyWithBpnStream.map(fieldSelector) + .filter(s -> StringUtils.startsWithIgnoreCase(s, value)) + .distinct() + .sorted() + .limit(limit) + .toList(); + } + } + + private Stream getPolicyWithBpnStream(final Map> bpnToPoliciesMap) { + return bpnToPoliciesMap.entrySet() + .stream() + .flatMap(bpnWithPolicies -> bpnWithPolicies.getValue() + .stream() + .map(policy -> new PolicyWithBpn( + bpnWithPolicies.getKey(), policy))); + } + + private Function getFieldSelector(final String field) { + + final Function fieldSelector; + + if (PROPERTY_BPN.equalsIgnoreCase(field)) { + fieldSelector = PolicyWithBpn::bpn; + } else if (PROPERTY_POLICY_ID.equalsIgnoreCase(field)) { + fieldSelector = p -> p.policy().getPolicyId(); + } else if (PROPERTY_CREATED_ON.equalsIgnoreCase(field)) { + fieldSelector = p -> DateTimeFormatter.ofPattern("yyyy-MM-dd").format(p.policy().getCreatedOn()); + } else if (PROPERTY_VALID_UNTIL.equalsIgnoreCase(field)) { + fieldSelector = p -> DateTimeFormatter.ofPattern("yyyy-MM-dd").format(p.policy().getValidUntil()); + } else if (PROPERTY_ACTION.equalsIgnoreCase(field)) { + fieldSelector = p -> { + final List permissions = p.policy().getPermissions(); + return permissions == null || permissions.isEmpty() ? null : permissions.get(0).getAction().getValue(); + }; + } else { + log.warn("Field '{}' does not support autocomplete", field); + throw new IllegalArgumentException("Field does not support autocomplete"); + } + + return fieldSelector; + } + /** * Builder for {@link Comparator} for sorting a list of {@link PolicyWithBpn} objects. */