Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions js/libs/keycloak-admin-client/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@
"createdTimestamp" : {
"type" : "integer",
"format" : "int64",
"description" : "Timestamp when the client was created. Validation: on update/on patch: createdTimestamp is server-managed and must not be user-specified",
"description" : "Timestamp when the client was created",
"readOnly" : true
},
"updatedTimestamp" : {
"type" : "integer",
"format" : "int64",
"description" : "Timestamp when the client was last updated. Validation: on update/on patch: updatedTimestamp is server-managed and must not be user-specified",
"description" : "Timestamp when the client was last updated",
"readOnly" : true
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.keycloak.admin.api;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -37,8 +36,6 @@ public class ListOptions {
@QueryParam("offset")
protected Integer offset;

private transient List<SortSegment> parsedSortSegments;

public ListOptions fields(Set<String> fields) {
this.setFields(fields);
return this;
Expand All @@ -54,11 +51,6 @@ public ListOptions limit(int limit) {
return this;
}

public <F extends SortField> ListOptions sort(List<SortOption<F>> sort) {
this.setSort(sort);
return this;
}

public ListOptions offset(int offset) {
this.setOffset(offset);
return this;
Expand Down Expand Up @@ -101,91 +93,28 @@ public Integer getOffset() {
public void setOffset(Integer offset) {
this.offset = offset;
}

/**
* Parses the raw {@code sort} query parameter into an ordered list of segments
* (field name + direction), without resolving field names against any
* resource-specific field set. Use {@link #getSort(SortFieldResolver)} to resolve
* segments into typed {@link SortOption}s.
*/
public List<SortSegment> getSortSegments() {
if (sort == null) {
return null;
}
if (sort.isEmpty()) {
return List.of();
}
if (parsedSortSegments != null) {
return parsedSortSegments;
}
parsedSortSegments = Arrays.stream(sort.split(","))
.map(String::trim)
.filter(segment -> !segment.isEmpty())
.map(ListOptions::parseSortSegment)
.collect(Collectors.toUnmodifiableList());
if (parsedSortSegments.isEmpty()) {
throw new IllegalArgumentException("sort must specify at least one field");
}
return parsedSortSegments;
}

/**
* Resolves the parsed {@code sort} segments against a resource-specific set of
* sortable fields.
*
* @throws IllegalArgumentException if a segment's field name cannot be resolved by {@code resolver}
*/
public <F extends SortField> List<SortOption<F>> getSort(SortFieldResolver<F> resolver) {
List<SortSegment> segments = getSortSegments();
if (segments == null) {
return null;
}
if (segments.isEmpty()) {
return List.of();
}
return segments.stream()
.map(segment -> {
F field = resolver.resolve(segment.fieldName()).orElseThrow(() ->
new IllegalArgumentException(String.format("%s is not a sortable field", segment.fieldName())));
return SortOption.of(field, segment.order());
})
.collect(Collectors.toUnmodifiableList());
}

public <F extends SortField> void setSort(List<SortOption<F>> sort) {
if (sort == null) {
parsedSortSegments = null;
this.sort = null;
} else if (sort.isEmpty()) {
parsedSortSegments = List.of();
this.sort = "";
} else {
parsedSortSegments = sort.stream()
.map(option -> new SortSegment(option.field().toQueryValue(), option.order()))
.collect(Collectors.toUnmodifiableList());
this.sort = sort.stream().map(SortOption::toQuerySegment).collect(Collectors.joining(","));
}

public String getSort() {
return sort;
}

private static SortSegment parseSortSegment(String segment) {
String[] parts = segment.split("\\|", 2);
String fieldName = parts[0].trim();
if (fieldName.isEmpty()) {
throw new IllegalArgumentException("sort must specify at least one field");
}
SortOrder order = parts.length == 1 ? SortOrder.ASC : parseSortOrder(parts[1].trim());
return new SortSegment(fieldName, order);

public void setSort(String sort) {
this.sort = sort;
}

private static SortOrder parseSortOrder(String value) {
if (value.isEmpty()) {
return SortOrder.ASC;

public ListOptions addSortField(String field) {
return addSortField(field, true);
}

public ListOptions addSortField(String field, boolean ascending) {
if (this.sort == null) {
this.sort = "";
}
for (SortOrder order : SortOrder.values()) {
if (order.name().equalsIgnoreCase(value)) {
return order;
}
if (!this.sort.isEmpty()) {
this.sort += ",";
}
throw new IllegalArgumentException("sort direction must be asc or desc");
this.sort += field + (ascending ? "" : "|DESC");
return this;
}

}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
@JsonSubTypes.Type(value = SAMLClientRepresentation.class, name = SAMLClientRepresentation.PROTOCOL)
})
@ServerManagedFieldUnmodified(
affectedFieldNames = {"uuid", "createdTimestamp", "updatedTimestamp"},
affectedFieldNames = {"uuid"},
Comment thread
shawkins marked this conversation as resolved.
Resolved
groups = {PutClient.class, PatchClient.class})
//protocol is technically not server managed, but we can reuse the existing validation
@ServerManagedFieldUnmodified(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
package org.keycloak.admin.api;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ListOptionsTest {

private enum TestField implements SortField {
FOO,
BAR;

@Override
public String getApiName() {
return name().toLowerCase();
}

static Optional<TestField> fromApiName(String apiName) {
return Stream.of(values()).filter(field -> field.getApiName().equals(apiName)).findFirst();
}
}

@Test
void testGetFieldsEmpty() {
ListOptions options = new ListOptions();
Expand All @@ -43,74 +24,12 @@ void testGetFields() {
}

@Test
void testGetSortSegmentsEmpty() {
ListOptions options = new ListOptions();
options.setSort(List.<SortOption<TestField>>of());
assertEquals("", options.sort);
assertEquals(List.of(), options.getSortSegments());
}

@Test
void testGetSortSegments() {
void testAddSortFields() {
ListOptions options = new ListOptions();
options.sort = "bar|desc,foo";
assertEquals(List.of(new SortSegment("bar", SortOrder.DESC), new SortSegment("foo", SortOrder.ASC)),
options.getSortSegments());
}

@Test
void getSortSegmentsCachesParsedResult() {
ListOptions options = new ListOptions();
options.sort = "bar|desc,foo";

List<SortSegment> first = options.getSortSegments();
List<SortSegment> second = options.getSortSegments();

assertEquals(List.of(new SortSegment("bar", SortOrder.DESC), new SortSegment("foo", SortOrder.ASC)), first);
assertSame(first, second);
}

@Test
void testGetSortResolvesFields() {
ListOptions options = new ListOptions();
options.sort = "bar|desc,foo";
assertEquals(List.of(SortOption.of(TestField.BAR, SortOrder.DESC), SortOption.of(TestField.FOO)),
options.getSort(TestField::fromApiName));
}

@Test
void testSetSort() {
ListOptions options = new ListOptions();
options.setSort(List.of(SortOption.of(TestField.FOO, SortOrder.DESC)));
assertEquals("foo|desc", options.sort);
}

@Test
void invalidSortSegmentFormatThrowsBadRequest() {
ListOptions options = new ListOptions();
options.sort = "|desc";

IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, options::getSortSegments);
assertEquals("sort must specify at least one field", exception.getMessage());
}

@Test
void invalidSortFieldThrowsBadRequest() {
ListOptions options = new ListOptions();
options.sort = "unknown";

IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> options.getSort(TestField::fromApiName));
assertEquals("unknown is not a sortable field", exception.getMessage());
}

@Test
void invalidSortDirectionThrowsBadRequest() {
ListOptions options = new ListOptions();
options.sort = "foo|what";

IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, options::getSortSegments);
assertEquals("sort direction must be asc or desc", exception.getMessage());
options.addSortField("field");
assertEquals("field", options.getSort());
options.addSortField("field2", false);
assertEquals("field,field2|DESC", options.getSort());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.keycloak.services.client.ClientField;
import org.keycloak.services.client.ClientSortField;

import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.annotation.JsonSubTypes;
Expand Down Expand Up @@ -42,7 +42,7 @@ public class OASModelFilter implements OASFilter {
private static final String ALLOWED_FIELDS_MARKER = "Allowed fields: ";
private static final String CLIENTS_LIST_PATH_SUFFIX = "/clients/v2";
private static final Map<String, Supplier<String>> SORT_ALLOWED_FIELDS_BY_PATH_SUFFIX = Map.of(
CLIENTS_LIST_PATH_SUFFIX, ClientField::allowedApiNames
CLIENTS_LIST_PATH_SUFFIX, ClientSortField::allowedApiNames
);

private final Logger log = Logger.getLogger(OASModelFilter.class);
Expand Down
Loading
Loading