Skip to content

Commit 36d37cb

Browse files
committed
Add pagination helpers to PageRequest and javadoc to FilterResponse
PageRequest gains getZeroBasedStartIndex() and getEffectiveCount(int) to eliminate the error-prone 1-based to 0-based conversion that every Repository implementation must duplicate. FilterResponse gets javadoc clarifying that totalResults must be the total count of ALL matching resources before pagination, not the page size. References RFC 7644 §3.4.2.4. Generated-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3bd6067 commit 36d37cb

3 files changed

Lines changed: 147 additions & 3 deletions

File tree

scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterResponse.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,39 @@
2121

2222
import java.util.Collection;
2323

24+
/**
25+
* Holds the result of a {@link org.apache.directory.scim.core.repository.Repository#find Repository.find()}
26+
* query, including the paginated resources and the total count of matching resources.
27+
*
28+
* <p><b>Important:</b> {@code totalResults} must be the total number of resources matching
29+
* the query <em>before</em> pagination is applied, not the number of resources in this page.
30+
* This allows SCIM clients to calculate how many pages exist. See
31+
* <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4">RFC 7644 §3.4.2.4</a>.</p>
32+
*
33+
* <p>Example: if 50 users match a filter and the client requests {@code startIndex=11&count=10},
34+
* then {@code resources} contains 10 users and {@code totalResults} is 50.</p>
35+
*
36+
* @param <T> the resource type
37+
*/
2438
public class FilterResponse<T> {
25-
39+
2640
private Collection<T> resources;
41+
42+
/**
43+
* The total number of resources matching the query, before pagination.
44+
* This is NOT the size of the {@link #resources} collection (which is the page size).
45+
*/
2746
private int totalResults;
28-
47+
2948
public FilterResponse() {}
30-
49+
50+
/**
51+
* Creates a filter response with the given page of resources and total count.
52+
*
53+
* @param resources the resources in this page (may be a subset of all matching resources)
54+
* @param totalResults the total number of matching resources <em>before</em> pagination —
55+
* must be {@code >= resources.size()}
56+
*/
3157
public FilterResponse(Collection<T> resources, int totalResults) {
3258
this.resources = resources;
3359
this.totalResults = totalResults;

scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/PageRequest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,39 @@ public int hashCode() {
6868
return result;
6969
}
7070

71+
/**
72+
* Converts the 1-based SCIM {@code startIndex} to a 0-based offset suitable for
73+
* Java stream {@code skip()} or list {@code subList()} operations.
74+
*
75+
* <p>SCIM uses 1-based indexing: {@code startIndex=1} means the first result.
76+
* Per <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4">RFC 7644 §3.4.2.4</a>,
77+
* a value less than 1 is interpreted as 1 (offset 0). Returns 0 if
78+
* {@code startIndex} is null.</p>
79+
*
80+
* @return the 0-based offset (always {@code >= 0})
81+
*/
82+
public long getZeroBasedStartIndex() {
83+
return startIndex != null ? Math.max(0, startIndex - 1L) : 0L;
84+
}
85+
86+
/**
87+
* Returns the effective page size, defaulting to {@code totalResults} if the
88+
* client did not specify a {@code count} parameter.
89+
*
90+
* <p>Per <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4">RFC 7644 §3.4.2.4</a>,
91+
* a {@code count} of 0 means "return no resources" (only {@code totalResults}),
92+
* and a negative value is interpreted as 0.</p>
93+
*
94+
* @param totalResults the total number of matching resources (before pagination)
95+
* @return the effective page size (always {@code >= 0})
96+
*/
97+
public long getEffectiveCount(int totalResults) {
98+
if (count == null) {
99+
return (long) totalResults;
100+
}
101+
return Math.max(0L, count.longValue());
102+
}
103+
71104
public String toString() {
72105
return "PageRequest(startIndex=" + this.getStartIndex() + ", count=" + this.getCount() + ")";
73106
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.directory.scim.spec.filter;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
class PageRequestTest {
27+
28+
@Test
29+
void getZeroBasedStartIndex_withStartIndex1_returns0() {
30+
PageRequest pageRequest = new PageRequest().setStartIndex(1);
31+
32+
assertThat(pageRequest.getZeroBasedStartIndex()).isEqualTo(0L);
33+
}
34+
35+
@Test
36+
void getZeroBasedStartIndex_withStartIndex5_returns4() {
37+
PageRequest pageRequest = new PageRequest().setStartIndex(5);
38+
39+
assertThat(pageRequest.getZeroBasedStartIndex()).isEqualTo(4L);
40+
}
41+
42+
@Test
43+
void getZeroBasedStartIndex_withNullStartIndex_returns0() {
44+
PageRequest pageRequest = new PageRequest();
45+
46+
assertThat(pageRequest.getZeroBasedStartIndex()).isEqualTo(0L);
47+
}
48+
49+
@Test
50+
void getZeroBasedStartIndex_withStartIndex0_returns0_clamped() {
51+
PageRequest pageRequest = new PageRequest().setStartIndex(0);
52+
53+
assertThat(pageRequest.getZeroBasedStartIndex()).isEqualTo(0L);
54+
}
55+
56+
@Test
57+
void getEffectiveCount_withCount10_andTotalResults50_returns10() {
58+
PageRequest pageRequest = new PageRequest().setCount(10);
59+
60+
assertThat(pageRequest.getEffectiveCount(50)).isEqualTo(10L);
61+
}
62+
63+
@Test
64+
void getEffectiveCount_withNullCount_andTotalResults50_returns50() {
65+
PageRequest pageRequest = new PageRequest();
66+
67+
assertThat(pageRequest.getEffectiveCount(50)).isEqualTo(50L);
68+
}
69+
70+
@Test
71+
void getEffectiveCount_withCount0_returns0() {
72+
// RFC 7644 §3.4.2.4: count=0 means "return no resources"
73+
PageRequest pageRequest = new PageRequest().setCount(0);
74+
75+
assertThat(pageRequest.getEffectiveCount(50)).isEqualTo(0L);
76+
}
77+
78+
@Test
79+
void getEffectiveCount_withNegativeCount_returns0() {
80+
// RFC 7644 §3.4.2.4: negative count interpreted as 0
81+
PageRequest pageRequest = new PageRequest().setCount(-1);
82+
83+
assertThat(pageRequest.getEffectiveCount(50)).isEqualTo(0L);
84+
}
85+
}

0 commit comments

Comments
 (0)