Skip to content
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

Feature/750 policy api field autocomplete #799

Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ _**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

### Fixed

- Access and Usage Policy Validation flow correction. #757
Expand Down
68 changes: 68 additions & 0 deletions docs/src/api/irs-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -211,6 +213,53 @@ public Map<String, List<PolicyResponse>> 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<String> 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<String, List<Policy>> bpnToPoliciesMap = service.getPolicies(null);
return policyPagingService.autocomplete(bpnToPoliciesMap, field, value, limit);

}

@GetMapping("/policies/paged")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "Find policies.", //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,30 +69,71 @@ public Page<PolicyWithBpn> getPolicies(final Map<String, List<Policy>> bpnToPoli

final Comparator<PolicyWithBpn> comparator = new PolicyComparatorBuilder(pageable).build();
final Predicate<PolicyWithBpn> filter = new PolicyFilterBuilder(searchCriteria).build();

final List<PolicyWithBpn> policies = bpnToPoliciesMap.entrySet()
.stream()
.flatMap(bpnWithPolicies -> bpnWithPolicies.getValue()
.stream()
.map(policy -> new PolicyWithBpn(
bpnWithPolicies.getKey(),
policy)))
.filter(filter)
.sorted(comparator)
.toList();

final List<PolicyWithBpn> policies = getPolicyWithBpnStream(bpnToPoliciesMap).filter(filter)
.sorted(comparator)
.toList();
return applyPaging(pageable, policies);
}

private PageImpl<PolicyWithBpn> applyPaging(final Pageable pageable, final List<PolicyWithBpn> policies) {
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<PolicyWithBpn> pagedPolicies = policies.subList(start, end);

return new PageImpl<>(pagedPolicies,
PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), pageable.getSort()), policies.size());
}

public List<String> autocomplete(final Map<String, List<Policy>> 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<PolicyWithBpn, String> fieldSelector = getFieldSelector(field);
final Stream<PolicyWithBpn> policyWithBpnStream = getPolicyWithBpnStream(bpnToPoliciesMap);
return policyWithBpnStream.map(fieldSelector)
.filter(s -> StringUtils.startsWithIgnoreCase(s, value))
.distinct()
.sorted()
.limit(limit)
.toList();
}
}

private Stream<PolicyWithBpn> getPolicyWithBpnStream(final Map<String, List<Policy>> bpnToPoliciesMap) {
return bpnToPoliciesMap.entrySet()
.stream()
.flatMap(bpnWithPolicies -> bpnWithPolicies.getValue()
.stream()
.map(policy -> new PolicyWithBpn(
bpnWithPolicies.getKey(), policy)));
}

private Function<PolicyWithBpn, String> getFieldSelector(final String field) {

final Function<PolicyWithBpn, String> 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<Permission> 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.
*/
Expand Down
47 changes: 47 additions & 0 deletions local/testing/request-collection/IRS_Request_Collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading