Skip to content

Commit

Permalink
feat(policy-api):[eclipse-tractusx#639] Add a paged GET policies endp…
Browse files Browse the repository at this point in the history
…oint

Filtering is still missing
  • Loading branch information
dsmf committed Jul 2, 2024
1 parent fd3ff5f commit 258d1cd
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 4 deletions.
4 changes: 4 additions & 0 deletions irs-policy-store/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>net.javacrumbs.json-unit</groupId>
<artifactId>json-unit-assertj</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@
import org.eclipse.tractusx.irs.policystore.models.CreatePoliciesResponse;
import org.eclipse.tractusx.irs.policystore.models.CreatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.models.PolicyResponse;
import org.eclipse.tractusx.irs.policystore.models.PolicyWithBpn;
import org.eclipse.tractusx.irs.policystore.models.UpdatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.services.PolicyStoreService;
import org.eclipse.tractusx.irs.policystore.validators.BusinessPartnerNumberListValidator;
import org.eclipse.tractusx.irs.policystore.validators.ValidListOfBusinessPartnerNumbers;
import org.eclipse.tractusx.irs.policystore.validators.ValidPolicyId;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -88,6 +93,7 @@
public class PolicyStoreController {

public static final String BPN_REGEX = BusinessPartnerNumberListValidator.BPN_REGEX;
public static final int DEFAULT_PAGE_SIZE = 10;

private final PolicyStoreService service;

Expand Down Expand Up @@ -194,6 +200,20 @@ public Map<String, List<PolicyResponse>> getPolicies(//
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
}

@GetMapping("/policies/paged")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAuthority('" + IrsRoles.ADMIN_IRS + "')")
public Page<PolicyResponse> getPoliciesPaged(//
@PageableDefault(size = DEFAULT_PAGE_SIZE, sort = "bpn", direction = Sort.Direction.ASC) //
final Pageable pageable, //
@RequestParam(required = false) //
@ValidListOfBusinessPartnerNumbers //
@Parameter(description = "List of business partner numbers.") //
final List<String> businessPartnerNumbers) {
final Page<PolicyWithBpn> policies = service.getPolicies(businessPartnerNumbers, pageable);
return policies.map(PolicyResponse::fromPolicyWithBpn);
}

@Operation(operationId = "deleteAllowedPolicy",
summary = "Removes a policy that should no longer be accepted in EDC negotiation.",
security = @SecurityRequirement(name = "api_key"), tags = { "Item Relationship Service" },
Expand Down Expand Up @@ -252,8 +272,7 @@ public void deleteAllowedPolicy(@ValidPolicyId @PathVariable("policyId") final S
@DeleteMapping("/policies/{policyId}/bpnl/{bpnl}")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasAuthority('" + IrsRoles.ADMIN_IRS + "')")
public void removeAllowedPolicyFromBpnl(
@PathVariable("policyId") final String policyId, //
public void removeAllowedPolicyFromBpnl(@PathVariable("policyId") final String policyId, //
@Pattern(regexp = BPN_REGEX, message = " Invalid BPN.") //
@PathVariable("bpnl") final String bpnl) {
service.deletePolicyForEachBpn(policyId, List.of(bpnl));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*/
@Builder
@Schema(example = PolicyResponse.EXAMPLE_PAYLOAD)
public record PolicyResponse(OffsetDateTime validUntil, Payload payload) {
public record PolicyResponse(OffsetDateTime validUntil, Payload payload, String bpn) {

public static final String BPN_TO_POLICY_MAP_EXAMPLE = """
{
Expand Down Expand Up @@ -143,4 +143,17 @@ public static PolicyResponse fromPolicy(final Policy policy) {
.build())
.build();
}

public static PolicyResponse fromPolicyWithBpn(final PolicyWithBpn policyWithBpn) {
return PolicyResponse.builder()
.validUntil(policyWithBpn.policy().getValidUntil())
.payload(Payload.builder()
.policyId(policyWithBpn.policy().getPolicyId())
.context(Context.getDefault())
.policy(policyWithBpn.policy())
.build())
.bpn(policyWithBpn.bpn())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/********************************************************************************
* Copyright (c) 2022,2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.policystore.models;

import org.eclipse.tractusx.irs.edc.client.policy.Policy;

/**
* Helper record for remembering the BPN the policy belongs to.
*
* @param bpn the business partner number
* @param policy the policy
*/
public record PolicyWithBpn(String bpn, Policy policy) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@
import org.eclipse.tractusx.irs.policystore.config.DefaultAcceptedPoliciesConfig;
import org.eclipse.tractusx.irs.policystore.exceptions.PolicyStoreException;
import org.eclipse.tractusx.irs.policystore.models.CreatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.models.PolicyWithBpn;
import org.eclipse.tractusx.irs.policystore.models.UpdatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.persistence.PolicyPersistence;
import org.eclipse.tractusx.irs.policystore.validators.PolicyValidator;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
Expand Down Expand Up @@ -137,6 +143,99 @@ public Policy registerPolicy(final CreatePolicyRequest request) {
}
}

/**
* Finds policies by list of BPN. Results are returned as pages.
*
* @param bpnList list of BPNs
* @param pageable the page request options
* @return a map that maps BPN to list of policies
*/
public Page<PolicyWithBpn> getPolicies(final List<String> bpnList, final Pageable pageable) {

Comparator<PolicyWithBpn> comparator = getComparator(getSortField(pageable));
if (!isFieldSortedAscending(pageable, getSortField(pageable))) {
comparator = comparator.reversed();
}

// data from the original request which maps BPN to list of policies
final Map<String, List<Policy>> bpnToPoliciesMap = getPolicies(bpnList);

final List<PolicyWithBpn> policies = bpnToPoliciesMap.entrySet()
.stream()
.flatMap(bpnWithPolicies -> bpnWithPolicies.getValue()
.stream()
.map(policy -> new PolicyWithBpn(
bpnWithPolicies.getKey(),
policy)))
.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);

final String sortField = getSortField(pageable);
final Sort sort = isFieldSortedAscending(pageable, sortField)
? Sort.by(sortField).ascending()
: Sort.by(sortField).descending();
return new PageImpl<>(pagedPolicies, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort),
policies.size());
}

private static Comparator<PolicyWithBpn> getComparator(final String sortField) {
Comparator<PolicyWithBpn> comparator;
if ("bpn".equals(sortField)) {
comparator = Comparator.comparing(PolicyWithBpn::bpn);
} else if ("validUntil".equals(sortField)) {
comparator = Comparator.comparing(p -> p.policy().getValidUntil());
} else if ("policyId".equals(sortField)) {
comparator = Comparator.comparing(p -> p.policy().getPolicyId());
} else if ("createdOn".equals(sortField)) {
comparator = Comparator.comparing(p -> p.policy().getCreatedOn());
} else if ("action".equals(sortField)) {
comparator = Comparator.comparing(p -> {
final List<Permission> permissions = p.policy().getPermissions();
return permissions.isEmpty() ? null : permissions.get(0).getAction();
});
} else {
throw new IllegalArgumentException("Sorting by this field is not supported");
}
return comparator;
}

private static String getSortField(final Pageable pageable) {
final String sortField;
final Sort requestedSort = pageable.getSort();
if (requestedSort.isUnsorted()) {
sortField = "bpn";
} else {
if (requestedSort.stream().count() > 1) {
throw new IllegalArgumentException("Currently only sorting by one field is supported");
}
sortField = requestedSort.toList().get(0).getProperty();
}
return sortField;
}

public boolean isFieldSortedAscending(final Pageable pageable, final String fieldName) {

if (pageable.getSort().isUnsorted()) {
return true;
}

final Sort sort = pageable.getSort();
for (Sort.Order order : sort) {
if (order.getProperty().equals(fieldName) && order.getDirection() == Sort.Direction.ASC) {
return true;
}
}
return false;
}

/**
* Finds policies by list of BPN.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ void registerAllowedPolicy() {
}

@Nested
class GetPolicyTests {
class GetPoliciesTests {

@Test
void getPolicies() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

import java.time.Clock;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -58,6 +59,7 @@
import org.eclipse.tractusx.irs.policystore.controllers.PolicyStoreControllerTest;
import org.eclipse.tractusx.irs.policystore.exceptions.PolicyStoreException;
import org.eclipse.tractusx.irs.policystore.models.CreatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.models.PolicyWithBpn;
import org.eclipse.tractusx.irs.policystore.models.UpdatePolicyRequest;
import org.eclipse.tractusx.irs.policystore.persistence.PolicyPersistence;
import org.eclipse.tractusx.irs.policystore.testutil.PolicyStoreTestUtil;
Expand All @@ -69,6 +71,9 @@
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

Expand Down Expand Up @@ -306,6 +311,114 @@ void getStoredPolicies_whenNoPoliciesForBpn_shouldReturnTheConfiguredDefaultPoli
}
}

@Nested
class GetPoliciesPagedTests {

private final Map<String, List<Policy>> policiesMap = Map.of( //
"BPN1", Arrays.asList( //
createDummyPolicy("policy-1"), //
createDummyPolicy("policy-4") //
), //
"BPN2", Arrays.asList( //
createDummyPolicy("policy-2"), //
createDummyPolicy("policy-3"), //
createDummyPolicy("policy-5") //
));

@Test
public void whenUnsorted_shouldUseDefaultSortingBpnAscending() {
when(persistenceMock.readAll()).thenReturn(policiesMap);

final Page<PolicyWithBpn> result = testee.getPolicies(null, PageRequest.of(0, 10));

assertThat(result.getContent().stream().map(PolicyWithBpn::bpn).toList()).containsExactly("BPN1", "BPN1",
"BPN2", "BPN2", "BPN2");
}

@Test
public void whenSortedByBpnAsc() {
when(persistenceMock.readAll()).thenReturn(policiesMap);

final Page<PolicyWithBpn> result = testee.getPolicies(null,
PageRequest.of(0, 10, Sort.by("bpn").ascending()));

assertThat(result.getContent().stream().map(PolicyWithBpn::bpn).toList()).containsExactly("BPN1", "BPN1",
"BPN2", "BPN2", "BPN2");
}

@Test
public void whenSortedByBpnDesc() {
when(persistenceMock.readAll()).thenReturn(policiesMap);

final Page<PolicyWithBpn> result = testee.getPolicies(null,
PageRequest.of(0, 10, Sort.by("bpn").descending()));

assertThat(result.getContent().stream().map(PolicyWithBpn::bpn).toList()).containsExactly("BPN2", "BPN2",
"BPN2", "BPN1", "BPN1");
}

@Test
public void whenRequestedPageIsAvailable_thenCorrectPageIsReturned() {
when(persistenceMock.readAll()).thenReturn(policiesMap);

final Page<PolicyWithBpn> result = testee.getPolicies(null, PageRequest.of(2, 2));

assertThat(result).isNotNull();
assertThat(result.getNumber()).isEqualTo(2);
assertThat(result.getTotalPages()).isEqualTo(3);
assertThat(result.getNumberOfElements()).isEqualTo(1);
assertThat(result.getTotalElements()).isEqualTo(5);
assertThat(result.getContent()).hasSize(1);
assertThat(result.isFirst()).isFalse();
assertThat(result.isLast()).isTrue();
}

@Test
public void whenNoPoliciesAvailableInDb_thenDefaultPolicyFromConfigIsReturned() {
when(persistenceMock.readAll()).thenReturn(emptyMap());

final Page<PolicyWithBpn> result = testee.getPolicies(null, PageRequest.of(0, 10));

assertThat(result).isNotNull();
assertThat(result.getNumber()).isEqualTo(0);
assertThat(result.getTotalPages()).isEqualTo(1);
assertThat(result.getNumberOfElements()).isEqualTo(1);
assertThat(result.getTotalElements()).isEqualTo(1);
assertThat(result.getContent()).isNotEmpty();
assertThat(result.isFirst()).isTrue();
assertThat(result.isLast()).isTrue();
}

@Test
public void whenPageRequestedBeyondAvailableData_thenReturnEmptyPage() {

// ARRANGE
when(persistenceMock.readAll()).thenReturn(policiesMap);

// ACT
final Page<PolicyWithBpn> result = testee.getPolicies(null, PageRequest.of(2, 10));

// ASSERT
assertThat(result).isNotNull();
assertThat(result.getNumber()).isEqualTo(2);
assertThat(result.getTotalPages()).isEqualTo(1);
assertThat(result.getNumberOfElements()).isEqualTo(0);
assertThat(result.getTotalElements()).isEqualTo(5);
assertThat(result.getContent()).isEmpty();
assertThat(result.isFirst()).isFalse();
assertThat(result.isLast()).isTrue();
}

private Policy createDummyPolicy(final String policyId) {
return Policy.builder()
.policyId(policyId)
.createdOn(OffsetDateTime.now())
.validUntil(OffsetDateTime.now())
.permissions(createPermissions())
.build();
}
}

@Nested
class GetAcceptedPoliciesTests {

Expand Down

0 comments on commit 258d1cd

Please sign in to comment.