From e76c5e2d9726d346bc1e27d008e25d2448f9dd31 Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Fri, 3 Apr 2026 15:02:13 -0400 Subject: [PATCH] Add sorting and pagination helpers for in-memory SCIM repositories Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scim/core/repository/Repository.java | 9 +- .../jersey4/service/InMemoryGroupService.java | 14 +- .../jersey4/service/InMemoryUserService.java | 14 +- .../jersey/service/InMemoryGroupService.java | 14 +- .../jersey/service/InMemoryUserService.java | 14 +- .../memory/service/InMemoryGroupService.java | 14 +- .../memory/service/InMemoryUserService.java | 14 +- .../quarkus/service/InMemoryGroupService.java | 14 +- .../quarkus/service/InMemoryUserService.java | 14 +- .../spring/service/InMemoryGroupService.java | 14 +- .../spring/service/InMemoryUserService.java | 14 +- .../spring/service/InMemoryGroupService.java | 14 +- .../spring/service/InMemoryUserService.java | 14 +- .../it/testapp/InMemoryGroupService.java | 14 +- .../it/testapp/InMemoryUserService.java | 14 +- .../scim/spec/filter/FilterResponse.java | 34 +++ .../scim/spec/filter/SortExpressions.java | 120 ++++++++ .../scim/spec/filter/FilterResponseTest.java | 94 ++++++ .../scim/spec/filter/SortExpressionsTest.java | 273 ++++++++++++++++++ .../spring/it/app/InMemoryGroupService.java | 14 +- .../spring/it/app/InMemoryUserService.java | 14 +- 21 files changed, 641 insertions(+), 113 deletions(-) create mode 100644 scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/SortExpressions.java create mode 100644 scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterResponseTest.java create mode 100644 scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/SortExpressionsTest.java diff --git a/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java b/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java index 11e5212b..57c8e165 100644 --- a/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java +++ b/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java @@ -104,7 +104,14 @@ public interface Repository { * may be truncated by the scope specified by the passed PageRequest and * the order of the returned resources may be controlled by the passed * SortRequest. - * + * + *

Sorting: If the request context contains a {@link org.apache.directory.scim.spec.filter.SortRequest}, + * the repository is responsible for applying it if the backend supports server-side + * sorting. The SCIM server layer does NOT apply post-retrieval sorting as a fallback. + * If the requested sort attribute is not supported, the repository should silently ignore + * the sort request and return results in the backend's natural order, per + * RFC 7644 §3.4.2.3.

+ * * @param filter The filter that determines the ScimResources that will be * part of the ResultList. * @param requestContext the context object holding additional information about the request. diff --git a/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryGroupService.java b/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryGroupService.java index b9d29fc0..6d79b1cc 100644 --- a/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryGroupService.java @@ -35,7 +35,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -115,12 +116,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryUserService.java b/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryUserService.java index 0201b7f2..d9674408 100644 --- a/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-jersey-4/src/main/java/org/apache/directory/scim/example/jersey4/service/InMemoryUserService.java @@ -36,7 +36,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -148,12 +149,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java index dccfc1a3..2c595969 100644 --- a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java @@ -35,7 +35,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -115,12 +116,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java index a9101805..f58ef917 100644 --- a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java @@ -36,7 +36,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -148,12 +149,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java index ca7b1a8b..5c8065e6 100644 --- a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java @@ -35,7 +35,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -115,12 +116,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java index 795f0eb7..0c73736d 100644 --- a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java @@ -36,7 +36,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -148,12 +149,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java index e4b4c224..acdbfadb 100644 --- a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java @@ -35,7 +35,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -115,12 +116,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java index 039ce749..e2a61ff6 100644 --- a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java @@ -36,7 +36,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -148,12 +149,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java b/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java index f017c78a..cfb1536c 100644 --- a/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java @@ -31,7 +31,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; import org.springframework.stereotype.Service; @@ -109,12 +110,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java b/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java index 93592898..4629c59b 100644 --- a/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-spring-boot-4/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java @@ -33,7 +33,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -142,12 +143,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java index f017c78a..cfb1536c 100644 --- a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java +++ b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java @@ -31,7 +31,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; import org.springframework.stereotype.Service; @@ -109,12 +110,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java index 93592898..4629c59b 100644 --- a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java +++ b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java @@ -33,7 +33,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; import org.apache.directory.scim.spec.resources.ScimExtension; @@ -142,12 +143,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java index 6deb7fc6..dfa2adf8 100644 --- a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java +++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java @@ -35,7 +35,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.patch.PatchOperation; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -133,12 +134,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java index 9576f436..cdc40d18 100644 --- a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java +++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java @@ -34,7 +34,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.patch.PatchOperation; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; @@ -161,12 +162,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterResponse.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterResponse.java index a8d893c0..38d69904 100644 --- a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterResponse.java +++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/FilterResponse.java @@ -19,7 +19,10 @@ package org.apache.directory.scim.spec.filter; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.stream.Collector; /** * Holds the result of a {@link org.apache.directory.scim.core.repository.Repository#find Repository.find()} @@ -37,6 +40,37 @@ */ public class FilterResponse { + /** + * Returns a {@link Collector} that accumulates stream elements and applies pagination, + * producing a {@link FilterResponse} whose {@code totalResults} reflects the pre-pagination + * count. + * + *

This is intended for in-memory or demo implementations. Production repositories + * should push pagination into the data store's query language.

+ * + *

Usage:

+ *
{@code
+   * return items.stream()
+   *     .filter(FilterExpressions.inMemory(filter, schema))
+   *     .sorted(SortExpressions.comparator(sortRequest, schema))
+   *     .collect(FilterResponse.paginate(pageRequest));
+   * }
+ * + * @param pageRequest the pagination parameters (startIndex and count) + * @param the resource type + * @return a collector that produces a paginated {@code FilterResponse} + * @see PageRequest#paginate(List) + * @see RFC 7644 §3.4.2.4 + */ + public static Collector> paginate(PageRequest pageRequest) { + return Collector., FilterResponse>of( + ArrayList::new, + List::add, + (a, b) -> { a.addAll(b); return a; }, + list -> new FilterResponse<>(pageRequest.paginate(list), list.size()) + ); + } + private Collection resources; /** diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/SortExpressions.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/SortExpressions.java new file mode 100644 index 00000000..fcb1c3de --- /dev/null +++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/filter/SortExpressions.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + + * http://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. + */ + +package org.apache.directory.scim.spec.filter; + +import org.apache.directory.scim.spec.filter.attribute.AttributeReference; +import org.apache.directory.scim.spec.resources.ScimResource; +import org.apache.directory.scim.spec.schema.Schema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Comparator; + +/** + * Converts a {@link SortRequest} into a {@link Comparator} used for in-memory sorting. Production implementations + * should translate the SortRequest into the appropriate query language (e.g., SQL ORDER BY). + *

+ * + * This implementation should only be used for small collections or demo purposes. + * + * @see RFC 7644 Section 3.4.2.3 - Sorting + */ +public final class SortExpressions { + + private static final Logger log = LoggerFactory.getLogger(SortExpressions.class); + + private SortExpressions() {} + + /** + * Converts a {@link SortRequest} into a {@link Comparator} for in-memory evaluation. + *

+ * If the {@code sortRequest} is {@code null} or has no {@code sortBy}, a no-op comparator is returned + * that preserves the original order. Per RFC 7644 Section 3.4.2.3, when {@code sortOrder} is not specified, + * it defaults to {@link SortOrder#ASCENDING}. + * + * @param sortRequest the sort request containing sortBy and sortOrder + * @param schema the schema to resolve attributes against + * @return a comparator for sorting ScimResources + */ + public static Comparator comparator(SortRequest sortRequest, Schema schema) { + if (sortRequest == null || sortRequest.getSortBy() == null) { + return (a, b) -> 0; + } + + AttributeReference sortBy = sortRequest.getSortBy(); + SortOrder sortOrder = sortRequest.getSortOrder() != null ? sortRequest.getSortOrder() : SortOrder.ASCENDING; + + Schema.Attribute schemaAttribute = BaseFilterExpressionMapper.attribute(schema, sortBy); + if (schemaAttribute == null) { + log.debug("Sort attribute '{}' not found in schema, preserving original order", sortBy); + return (a, b) -> 0; + } + + boolean hasSubAttribute = sortBy.getSubAttributeName() != null; + Schema.Attribute parentAttribute = hasSubAttribute ? schema.getAttribute(sortBy.getAttributeName()) : null; + + return (a, b) -> { + Object valueA = extractValue(a, schemaAttribute, parentAttribute, hasSubAttribute); + Object valueB = extractValue(b, schemaAttribute, parentAttribute, hasSubAttribute); + + // nulls always sort to end, regardless of sort order + if (valueA == null && valueB == null) return 0; + if (valueA == null) return 1; + if (valueB == null) return -1; + + int result = compareValues(valueA, valueB, schemaAttribute); + return sortOrder == SortOrder.DESCENDING ? -result : result; + }; + } + + private static Object extractValue(Object resource, Schema.Attribute attribute, Schema.Attribute parentAttribute, boolean hasSubAttribute) { + try { + if (hasSubAttribute && parentAttribute != null) { + Object parent = parentAttribute.getAccessor().get(resource); + if (parent == null) { + return null; + } + return attribute.getAccessor().get(parent); + } + return attribute.getAccessor().get(resource); + } catch (Exception e) { + log.debug("Failed to extract sort value", e); + return null; + } + } + + @SuppressWarnings("unchecked") + private static int compareValues(Object a, Object b, Schema.Attribute attribute) { + // case-insensitive string comparison when not caseExact + if (a instanceof String stringA && b instanceof String stringB) { + if (!attribute.isCaseExact()) { + return String.CASE_INSENSITIVE_ORDER.compare(stringA, stringB); + } + return stringA.compareTo(stringB); + } + + // Comparable types (LocalDateTime, Integer, etc.) + if (a instanceof Comparable comparableA && a.getClass().isInstance(b)) { + return comparableA.compareTo(b); + } + + return 0; + } +} diff --git a/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterResponseTest.java b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterResponseTest.java new file mode 100644 index 00000000..5deff399 --- /dev/null +++ b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/FilterResponseTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + + * http://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. + */ + +package org.apache.directory.scim.spec.filter; + +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class FilterResponseTest { + + @Test + void paginate_defaultPageRequest_returnsAll() { + FilterResponse response = Stream.of("a", "b", "c", "d", "e") + .collect(FilterResponse.paginate(new PageRequest())); + + assertThat(response.getResources()).containsExactly("a", "b", "c", "d", "e"); + assertThat(response.getTotalResults()).isEqualTo(5); + } + + @Test + void paginate_specificPage_returnsSlice() { + FilterResponse response = Stream.of("a", "b", "c", "d", "e") + .collect(FilterResponse.paginate(new PageRequest().setStartIndex(2).setCount(2))); + + assertThat(response.getResources()).containsExactly("b", "c"); + assertThat(response.getTotalResults()).isEqualTo(5); + } + + @Test + void paginate_startIndexBeyondResults_returnsEmpty() { + FilterResponse response = Stream.of("a", "b", "c") + .collect(FilterResponse.paginate(new PageRequest().setStartIndex(10).setCount(2))); + + assertThat(response.getResources()).isEmpty(); + assertThat(response.getTotalResults()).isEqualTo(3); + } + + @Test + void paginate_countZero_returnsEmptyWithTotalResults() { + FilterResponse response = Stream.of("a", "b", "c") + .collect(FilterResponse.paginate(new PageRequest().setCount(0))); + + assertThat(response.getResources()).isEmpty(); + assertThat(response.getTotalResults()).isEqualTo(3); + } + + @Test + void paginate_emptyStream_returnsEmptyResponse() { + FilterResponse response = Stream.empty() + .collect(FilterResponse.paginate(new PageRequest())); + + assertThat(response.getResources()).isEmpty(); + assertThat(response.getTotalResults()).isEqualTo(0); + } + + @Test + void paginate_parallelStream_correctResults() { + FilterResponse response = Stream.iterate(1, i -> i + 1) + .limit(100) + .parallel() + .collect(FilterResponse.paginate(new PageRequest().setStartIndex(1).setCount(10))); + + assertThat(response.getResources()).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + assertThat(response.getTotalResults()).isEqualTo(100); + } + + @Test + void paginate_countExceedsRemaining_returnsAvailable() { + FilterResponse response = Stream.of("a", "b", "c") + .collect(FilterResponse.paginate(new PageRequest().setStartIndex(2).setCount(10))); + + assertThat(response.getResources()).containsExactly("b", "c"); + assertThat(response.getTotalResults()).isEqualTo(3); + } +} diff --git a/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/SortExpressionsTest.java b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/SortExpressionsTest.java new file mode 100644 index 00000000..f9ee486d --- /dev/null +++ b/scim-spec/scim-spec-schema/src/test/java/org/apache/directory/scim/spec/filter/SortExpressionsTest.java @@ -0,0 +1,273 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + + * http://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. + */ + +package org.apache.directory.scim.spec.filter; + +import org.apache.directory.scim.spec.filter.attribute.AttributeReference; +import org.apache.directory.scim.spec.resources.Email; +import org.apache.directory.scim.spec.resources.Name; +import org.apache.directory.scim.spec.resources.ScimResource; +import org.apache.directory.scim.spec.resources.ScimUser; +import org.apache.directory.scim.spec.schema.Meta; +import org.apache.directory.scim.spec.schema.Schema; +import org.apache.directory.scim.spec.schema.Schemas; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SortExpressions#comparator(SortRequest, Schema)}, which converts a SCIM + * {@link SortRequest} into a {@link Comparator} for in-memory sorting of {@link ScimResource}s. + * Covers ascending/descending order, sub-attributes, DateTime sorting, case-insensitive string + * comparison, null handling, unknown attributes, and composition with filtering and pagination. + * + * @see RFC 7644 Section 3.4.2.3 - Sorting + */ +class SortExpressionsTest { + + private static final Schema USER_SCHEMA = Schemas.schemaFor(ScimUser.class); + + private static final LocalDateTime NOW = LocalDateTime.now(); + + private static final ScimUser ALICE = user("alice", "Zulu", NOW.minusDays(2), NOW.minusDays(3)); + private static final ScimUser BOB = user("bob", "Alpha", NOW, NOW); + private static final ScimUser CHARLIE = user("Charlie", "Mango", NOW.minusDays(1), NOW.minusDays(2)); + private static final ScimUser DAVE = userNoName("dave"); + + private static final List ALL_USERS = List.of(ALICE, BOB, CHARLIE, DAVE); + + @Test + void defaultAscendingSortOrder() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")); + // sortOrder is null — RFC 7644 says default is ascending + + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + @Test + void explicitAscending() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")) + .setSortOrder(SortOrder.ASCENDING); + + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + @Test + void explicitDescending() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")) + .setSortOrder(SortOrder.DESCENDING); + + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("dave", "Charlie", "bob", "alice"); + } + + @Test + void dateTimeAttribute() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("meta.lastModified")) + .setSortOrder(SortOrder.ASCENDING); + + // DAVE has null lastModified, should sort to end + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("alice", "Charlie", "bob", "dave"); + } + + @Test + void descendingWithNullValuesAlwaysSortToEnd() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("meta.lastModified")) + .setSortOrder(SortOrder.DESCENDING); + + List sorted = sorted(ALL_USERS, request); + + // DAVE has null meta - should sort to END regardless of direction + assertThat(userNames(sorted)).containsExactly("bob", "Charlie", "alice", "dave"); + } + + @Test + void dateTimeCreatedAttribute() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("meta.created")) + .setSortOrder(SortOrder.ASCENDING); + + // DAVE has null meta, should sort to end + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("alice", "Charlie", "bob", "dave"); + } + + @Test + void subAttribute() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("name.familyName")) + .setSortOrder(SortOrder.ASCENDING); + + // DAVE has no name set, should sort to end + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("bob", "Charlie", "alice", "dave"); + } + + @Test + void caseInsensitiveStringSort() { + // userName is not caseExact, so "Charlie" should sort between "bob" and "dave" + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")) + .setSortOrder(SortOrder.ASCENDING); + + List sorted = sorted(ALL_USERS, request); + + // Case-insensitive: alice, bob, Charlie, dave (not Charlie, alice, bob, dave) + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + @Test + void unsupportedAttributePreservesOrder() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("nonExistent")) + .setSortOrder(SortOrder.ASCENDING); + + List sorted = sorted(ALL_USERS, request); + + // No-op comparator should preserve original order + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + @Test + void sortWithFilter() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")) + .setSortOrder(SortOrder.DESCENDING); + + Filter filter = FilterBuilder.create().startsWith("userName", "b").build(); + + List result = ALL_USERS.stream() + .map(ScimResource.class::cast) + .filter(FilterExpressions.inMemory(filter, USER_SCHEMA)) + .sorted(SortExpressions.comparator(request, USER_SCHEMA)) + .collect(Collectors.toList()); + + assertThat(userNames(result)).containsExactly("bob"); + } + + @Test + void sortWithPagination() { + SortRequest request = new SortRequest() + .setSortBy(new AttributeReference("userName")) + .setSortOrder(SortOrder.ASCENDING); + + List result = ALL_USERS.stream() + .map(ScimResource.class::cast) + .sorted(SortExpressions.comparator(request, USER_SCHEMA)) + .skip(1) + .limit(2) + .collect(Collectors.toList()); + + assertThat(userNames(result)).containsExactly("bob", "Charlie"); + } + + @Test + void nullSortRequestReturnsNoOpComparator() { + Comparator comparator = SortExpressions.comparator(null, USER_SCHEMA); + + List sorted = ALL_USERS.stream() + .map(ScimResource.class::cast) + .sorted(comparator) + .collect(Collectors.toList()); + + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + @Test + void nullSortByReturnsNoOpComparator() { + SortRequest request = new SortRequest() + .setSortOrder(SortOrder.ASCENDING); + + List sorted = sorted(ALL_USERS, request); + + assertThat(userNames(sorted)).containsExactly("alice", "bob", "Charlie", "dave"); + } + + private static List sorted(List users, SortRequest request) { + return users.stream() + .map(ScimResource.class::cast) + .sorted(SortExpressions.comparator(request, USER_SCHEMA)) + .collect(Collectors.toList()); + } + + private static List userNames(List resources) { + List names = new ArrayList<>(); + for (ScimResource r : resources) { + names.add(((ScimUser) r).getUserName()); + } + return names; + } + + private static ScimUser user(String username, String familyName, LocalDateTime lastModified, LocalDateTime created) { + ScimUser user = new ScimUser() + .setUserName(username) + .setActive(true) + .setName(new Name() + .setGivenName(username) + .setFamilyName(familyName) + ) + .setEmails(List.of( + new Email() + .setType("work") + .setPrimary(true) + .setValue(username + "@example.com") + )); + + user.setMeta(new Meta() + .setLastModified(lastModified) + .setCreated(created)); + + return user; + } + + private static ScimUser userNoName(String username) { + ScimUser user = new ScimUser() + .setUserName(username) + .setActive(true) + .setEmails(List.of( + new Email() + .setType("work") + .setPrimary(true) + .setValue(username + "@example.com") + )); + + // No name set, no meta set — tests null handling + return user; + } +} diff --git a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java index d6f4038e..11111bcb 100644 --- a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java +++ b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java @@ -32,7 +32,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.patch.PatchOperation; import org.apache.directory.scim.spec.resources.ScimExtension; import org.apache.directory.scim.spec.resources.ScimGroup; @@ -127,12 +128,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = groups.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimGroup.SCHEMA_URI); + return groups.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override diff --git a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java index 60e99d93..a1925e8d 100644 --- a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java +++ b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java @@ -31,7 +31,8 @@ import org.apache.directory.scim.spec.filter.Filter; import org.apache.directory.scim.spec.filter.FilterExpressions; import org.apache.directory.scim.spec.filter.FilterResponse; -import org.apache.directory.scim.spec.filter.PageRequest; +import org.apache.directory.scim.spec.filter.SortExpressions; +import org.apache.directory.scim.spec.schema.Schema; import org.apache.directory.scim.spec.patch.PatchOperation; import org.apache.directory.scim.spec.resources.Email; import org.apache.directory.scim.spec.resources.Name; @@ -159,12 +160,11 @@ public void delete(String id) throws ResourceException { @Override public FilterResponse find(Filter filter, ScimRequestContext requestContext) { - List filtered = users.values().stream() - .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI))) - .toList(); - - PageRequest pageRequest = requestContext.getPageRequestOrDefault(); - return new FilterResponse<>(pageRequest.paginate(filtered), filtered.size()); + Schema schema = schemaRegistry.getSchema(ScimUser.SCHEMA_URI); + return users.values().stream() + .filter(FilterExpressions.inMemory(filter, schema)) + .sorted(SortExpressions.comparator(requestContext.getSortRequest(), schema)) + .collect(FilterResponse.paginate(requestContext.getPageRequestOrDefault())); } @Override