diff --git a/CHANGELOG.md b/CHANGELOG.md index 98db99758..f1f9a67ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ _**For better traceability add the corresponding GitHub issue number in each cha ### Added +- Added autocomplete endpoint Policy Store API: `GET /irs/policies/attributes/{attribute}`. #750 - Added get and delete functionality for contract definitions eclipse-tractusx/traceability-foss#1190 ### Fixed diff --git a/docs/src/api/irs-api.yaml b/docs/src/api/irs-api.yaml index ac3b68acd..b29772ab2 100644 --- a/docs/src/api/irs-api.yaml +++ b/docs/src/api/irs-api.yaml @@ -972,6 +972,74 @@ paths: summary: Updates existing policies. tags: - Policy Store API + /irs/policies/attributes/{field}: + get: + description: Provides autocomplete suggestions for policy fields based on input + criteria. + operationId: autocomplete + parameters: + - description: "The field to autocomplete (BPN, policyId, createdOn, validUntil,\ + \ action)" + in: path + name: field + required: true + schema: + type: string + - description: Search query with restricted character set + in: query + name: s + required: true + schema: + type: string + pattern: "^[a-zA-Z0-9\\-\\+: ]*$" + - description: "Limit for the number of results, default is 10 and max is 100" + in: query + name: limit + required: false + schema: + type: integer + format: int32 + default: 10 + maximum: 100 + responses: + "200": + content: + application/json: + schema: + type: string + description: Successful retrieval of autocomplete suggestions + "400": + content: + application/json: + examples: + error: + $ref: '#/components/examples/error-response-403' + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Invalid input parameters + "401": + content: + application/json: + examples: + error: + $ref: '#/components/examples/error-response-401' + schema: + $ref: '#/components/schemas/ErrorResponse' + description: No valid authentication credentials. + "403": + content: + application/json: + examples: + error: + $ref: '#/components/examples/error-response-403' + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Authorization refused by server. + security: + - api_key: [] + summary: Autocomplete for policy fields + tags: + - Policy Store API /irs/policies/paged: get: description: | 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 99083688e..62384f48f 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; @@ -104,6 +105,7 @@ public class PolicyStoreController { public static final String SEARCH = "search"; public static final String POLICY_API_TAG = "Policy Store API"; public static final String API_KEY = "api_key"; + public static final int MAX_AUTOCOMPLETE_LIMIT = 100; private final PolicyStoreService service; @@ -211,6 +213,53 @@ 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.", + security = @SecurityRequirement(name = API_KEY), // + tags = { POLICY_API_TAG }, // + 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")) + }) + }) + 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 = MAX_AUTOCOMPLETE_LIMIT, + message = "Parameter 'limit' is above max") final int limit) { + + final Map> bpnToPoliciesMap = service.getPolicies(null); + return policyPagingService.autocomplete(bpnToPoliciesMap, field, value, limit); + + } + @GetMapping("/policies/paged") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Find policies.", // 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. */ diff --git a/local/testing/request-collection/IRS_Request_Collection.json b/local/testing/request-collection/IRS_Request_Collection.json index f937d7cfa..467b0c0d1 100644 --- a/local/testing/request-collection/IRS_Request_Collection.json +++ b/local/testing/request-collection/IRS_Request_Collection.json @@ -4,6 +4,53 @@ "__export_date": "2024-07-08T11:57:49.380Z", "__export_source": "insomnia.desktop.app:v9.3.2", "resources": [ + { + "_id": "req_3abf816e9c8c45518dd6c4f81e7bf1c2", + "parentId": "fld_ad061853620b45c397a7906a6e566930", + "modified": 1720439689711, + "created": 1720199730198, + "url": "{{IRS_HOST}}/irs/policies/attributes/{% prompt 'field', '', 'policyId', '', false, true %}", + "name": "Policy attribute auto-complete", + "description": "", + "method": "GET", + "body": {}, + "parameters": [ + { + "id": "pair_7f592a7e237a4ecf9590ddcbcc1aa71a", + "name": "limit", + "value": "{% prompt 'limit', '', '20', '', false, true %}", + "description": "", + "disabled": false + }, + { + "id": "pair_422cf5b352464645a70e05b1af10f874", + "name": "s", + "value": "{% prompt 's', '', '', '', false, true %}", + "description": "", + "disabled": false + } + ], + "headers": [ + ], + "authentication": { + "type": "apikey", + "disabled": false, + "key": "X-Api-Key", + "value": "{{ _.EDC_API_KEY }}", + "addTo": "header" + }, + "preRequestScript": "", + "metaSortKey": -1705005887717, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, { "_id": "req_a7c80b4809ac482ea6c7debfc7998505", "parentId": "fld_ad061853620b45c397a7906a6e566930",