Skip to content

Commit 42ec54d

Browse files
committed
Add SortExpressions for in-memory SCIM sorting
Converts a SortRequest into a Comparator<ScimResource> for use in stream pipelines. Documents sorting responsibility on Repository.find() -- repositories apply SortRequest if the backend supports server-side sorting; unsupported sort attributes are silently ignored per RFC 7644 S3.4.2.3. Generated-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a5eaad commit 42ec54d

3 files changed

Lines changed: 400 additions & 1 deletion

File tree

scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ public interface Repository<T extends ScimResource> {
104104
* may be truncated by the scope specified by the passed PageRequest and
105105
* the order of the returned resources may be controlled by the passed
106106
* SortRequest.
107-
*
107+
*
108+
* <p><b>Sorting:</b> If the request context contains a {@link org.apache.directory.scim.spec.filter.SortRequest},
109+
* the repository is responsible for applying it if the backend supports server-side
110+
* sorting. The SCIM server layer does NOT apply post-retrieval sorting as a fallback.
111+
* If the requested sort attribute is not supported, the repository should silently ignore
112+
* the sort request and return results in the backend's natural order, per
113+
* <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.3">RFC 7644 §3.4.2.3</a>.</p>
114+
*
108115
* @param filter The filter that determines the ScimResources that will be
109116
* part of the ResultList.
110117
* @param requestContext the context object holding additional information about the request.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.apache.directory.scim.spec.filter.attribute.AttributeReference;
23+
import org.apache.directory.scim.spec.resources.ScimResource;
24+
import org.apache.directory.scim.spec.schema.Schema;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import java.util.Comparator;
29+
30+
/**
31+
* Converts a {@link SortRequest} into a {@link Comparator} used for in-memory sorting. Production implementations
32+
* should translate the SortRequest into the appropriate query language (e.g., SQL ORDER BY).
33+
* <p>
34+
*
35+
* <b>This implementation should only be used for small collections or demo purposes.</b>
36+
*
37+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.3">RFC 7644 Section 3.4.2.3 - Sorting</a>
38+
*/
39+
public final class SortExpressions {
40+
41+
private static final Logger log = LoggerFactory.getLogger(SortExpressions.class);
42+
43+
private SortExpressions() {}
44+
45+
/**
46+
* Converts a {@link SortRequest} into a {@link Comparator} for in-memory evaluation.
47+
* <p>
48+
* If the {@code sortRequest} is {@code null} or has no {@code sortBy}, a no-op comparator is returned
49+
* that preserves the original order. Per RFC 7644 Section 3.4.2.3, when {@code sortOrder} is not specified,
50+
* it defaults to {@link SortOrder#ASCENDING}.
51+
*
52+
* @param sortRequest the sort request containing sortBy and sortOrder
53+
* @param schema the schema to resolve attributes against
54+
* @return a comparator for sorting ScimResources
55+
*/
56+
public static Comparator<ScimResource> comparator(SortRequest sortRequest, Schema schema) {
57+
if (sortRequest == null || sortRequest.getSortBy() == null) {
58+
return (a, b) -> 0;
59+
}
60+
61+
AttributeReference sortBy = sortRequest.getSortBy();
62+
SortOrder sortOrder = sortRequest.getSortOrder() != null ? sortRequest.getSortOrder() : SortOrder.ASCENDING;
63+
64+
Schema.Attribute schemaAttribute = BaseFilterExpressionMapper.attribute(schema, sortBy);
65+
if (schemaAttribute == null) {
66+
log.debug("Sort attribute '{}' not found in schema, preserving original order", sortBy);
67+
return (a, b) -> 0;
68+
}
69+
70+
boolean hasSubAttribute = sortBy.getSubAttributeName() != null;
71+
Schema.Attribute parentAttribute = hasSubAttribute ? schema.getAttribute(sortBy.getAttributeName()) : null;
72+
73+
Comparator<ScimResource> comparator = (a, b) -> {
74+
Object valueA = extractValue(a, schemaAttribute, parentAttribute, hasSubAttribute);
75+
Object valueB = extractValue(b, schemaAttribute, parentAttribute, hasSubAttribute);
76+
77+
return compareValues(valueA, valueB, schemaAttribute);
78+
};
79+
80+
if (sortOrder == SortOrder.DESCENDING) {
81+
comparator = comparator.reversed();
82+
}
83+
84+
return comparator;
85+
}
86+
87+
private static Object extractValue(Object resource, Schema.Attribute attribute, Schema.Attribute parentAttribute, boolean hasSubAttribute) {
88+
try {
89+
if (hasSubAttribute && parentAttribute != null) {
90+
Object parent = parentAttribute.getAccessor().get(resource);
91+
if (parent == null) {
92+
return null;
93+
}
94+
return attribute.getAccessor().get(parent);
95+
}
96+
return attribute.getAccessor().get(resource);
97+
} catch (Exception e) {
98+
log.debug("Failed to extract sort value", e);
99+
return null;
100+
}
101+
}
102+
103+
@SuppressWarnings("unchecked")
104+
private static int compareValues(Object a, Object b, Schema.Attribute attribute) {
105+
// nulls sort to end
106+
if (a == null && b == null) {
107+
return 0;
108+
}
109+
if (a == null) {
110+
return 1;
111+
}
112+
if (b == null) {
113+
return -1;
114+
}
115+
116+
// case-insensitive string comparison when not caseExact
117+
if (a instanceof String stringA && b instanceof String stringB) {
118+
if (!attribute.isCaseExact()) {
119+
return String.CASE_INSENSITIVE_ORDER.compare(stringA, stringB);
120+
}
121+
return stringA.compareTo(stringB);
122+
}
123+
124+
// Comparable types (LocalDateTime, Integer, etc.)
125+
if (a instanceof Comparable comparableA) {
126+
return comparableA.compareTo(b);
127+
}
128+
129+
return 0;
130+
}
131+
}

0 commit comments

Comments
 (0)