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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package uk.co.compendiumdev.thingifier.core.query;

import java.util.ArrayList;
import java.util.List;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;

public final class EntityInstanceListPaginator {

private final PaginationParams paginationParams;

public EntityInstanceListPaginator(final QueryFilterParams queryParams) {
paginationParams = new PaginationParams(queryParams);
}

public List<EntityInstance> paginate(final List<EntityInstance> foundItems) {
if (paginationParams.hasValidationError()) {
throw new IllegalArgumentException(paginationParams.validationError());
}

List<EntityInstance> items = new ArrayList<>(foundItems);
if (!paginationParams.hasLimit() && !paginationParams.hasOffset()) {
return items;
}

int limit = paginationParams.limitOr(items.size());
int offset = paginationParams.offsetOr(0);

if (limit == 0 || offset >= items.size()) {
return new ArrayList<>();
}

int toIndex = (int) Math.min((long) offset + limit, items.size());
return new ArrayList<>(items.subList(offset, toIndex));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public boolean matches(final EntityInstance instance) {
final EntityDefinition defn = instance.getEntity();

String fieldName = filterByCondition.fieldName;
if (SortByFieldName.isSortByParam(fieldName)
|| PaginationParams.isPaginationParam(fieldName)) {
continue;
}

// TODO: handle - ranges, like, or etc.
// currently all conditions are treated as an AND clause e.g. ?ID=<10&ID=>5 would be is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package uk.co.compendiumdev.thingifier.core.query;

public final class PaginationParams {
public static final String LIMIT_PARAMETER_NAME = "_limit";
public static final String OFFSET_PARAMETER_NAME = "_offset";

private Integer limit;
private Integer offset;
private String validationError;

public PaginationParams(final QueryFilterParams queryParams) {
QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;

for (FilterBy param : params.toList()) {
if (isLimitParam(param.fieldName)) {
limit = parseNonNegativeInteger(param, LIMIT_PARAMETER_NAME);
}
if (isOffsetParam(param.fieldName)) {
offset = parseNonNegativeInteger(param, OFFSET_PARAMETER_NAME);
}
}
}

public static boolean isPaginationParam(final String key) {
return isLimitParam(key) || isOffsetParam(key);
}

public static boolean isLimitParam(final String key) {
return LIMIT_PARAMETER_NAME.equals(key);
}

public static boolean isOffsetParam(final String key) {
return OFFSET_PARAMETER_NAME.equals(key);
}

public boolean hasLimit() {
return limit != null;
}

public boolean hasOffset() {
return offset != null;
}

public int limitOr(final int defaultLimit) {
return hasLimit() ? limit : defaultLimit;
}

public int offsetOr(final int defaultOffset) {
return hasOffset() ? offset : defaultOffset;
}

public boolean hasValidationError() {
return validationError != null;
}

public String validationError() {
return validationError;
}

private Integer parseNonNegativeInteger(final FilterBy param, final String parameterName) {
if (hasValidationError()) {
return null;
}

if (!"=".equals(param.filterOperation)) {
validationError = String.format("%s must be a non-negative integer", parameterName);
return null;
}

try {
int value = Integer.parseInt(param.fieldValue.trim());
if (value < 0) {
validationError = String.format("%s must be a non-negative integer", parameterName);
return null;
}
return value;
} catch (NumberFormatException e) {
validationError = String.format("%s must be a non-negative integer", parameterName);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ public List<FilterBy> sortBys() {
return sortCriteria;
}

public QueryFilterParams fieldFilters() {
QueryFilterParams fieldFilters = new QueryFilterParams();

for (FilterBy filterBy : filterBys) {
if (!isReservedQueryControl(filterBy.fieldName)) {
fieldFilters.add(
new FilterBy(
filterBy.fieldName,
filterBy.filterOperation + filterBy.fieldValue));
}
}

return fieldFilters;
}

public QueryFilterParams withoutPagingParams() {
QueryFilterParams params = new QueryFilterParams();

for (FilterBy filterBy : filterBys) {
if (!PaginationParams.isPaginationParam(filterBy.fieldName)) {
params.add(
new FilterBy(
filterBy.fieldName,
filterBy.filterOperation + filterBy.fieldValue));
}
}

return params;
}

public int size() {
return filterBys.size();
}
Expand Down Expand Up @@ -62,4 +92,9 @@ public boolean hasSortBy() {

return false;
}

private boolean isReservedQueryControl(final String fieldName) {
return SortByFieldName.isSortByParam(fieldName)
|| PaginationParams.isPaginationParam(fieldName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft;
import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListFilter;
import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListPaginator;
import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListSorter;
import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
import uk.co.compendiumdev.thingifier.core.reporting.ValidationReport;
Expand Down Expand Up @@ -147,7 +148,8 @@ List<EntityInstance> listInstances(
List<EntityInstance> instances = new ArrayList<>(listInstances(entity));
QueryFilterParams params = queryParams == null ? new QueryFilterParams() : queryParams;
instances = new EntityInstanceListFilter(params).filter(instances);
return new EntityInstanceListSorter(params).sort(instances);
instances = new EntityInstanceListSorter(params).sort(instances);
return new EntityInstanceListPaginator(params).paginate(instances);
}

int countInstances(final EntityDefinition entity) {
Expand Down Expand Up @@ -337,7 +339,8 @@ List<EntityInstance> listRelatedInstances(
relationships.listRelatedInstances(
instance, relationshipName, this::findByInternalId));
instances = new EntityInstanceListFilter(params).filter(instances);
return new EntityInstanceListSorter(params).sort(instances);
instances = new EntityInstanceListSorter(params).sort(instances);
return new EntityInstanceListPaginator(params).paginate(instances);
}

boolean hasRelationshipInstances(final EntityInstance instance) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
import uk.co.compendiumdev.thingifier.core.domain.instances.AutoIncrement;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstance;
import uk.co.compendiumdev.thingifier.core.domain.instances.EntityInstanceDraft;
import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListPaginator;
import uk.co.compendiumdev.thingifier.core.query.EntityInstanceListSorter;
import uk.co.compendiumdev.thingifier.core.query.EntityListSortParamParser;
import uk.co.compendiumdev.thingifier.core.query.FilterBy;
import uk.co.compendiumdev.thingifier.core.query.PaginationParams;
import uk.co.compendiumdev.thingifier.core.query.QueryFilterParams;
import uk.co.compendiumdev.thingifier.core.query.SortByFieldName;
import uk.co.compendiumdev.thingifier.core.reporting.RepositoryJsonExporter;
Expand Down Expand Up @@ -459,7 +461,9 @@ List<EntityInstance> listRelatedInstances(
}
}

return new EntityInstanceListSorter(params).sort(new ArrayList<>(related.values()));
List<EntityInstance> sorted =
new EntityInstanceListSorter(params).sort(new ArrayList<>(related.values()));
return new EntityInstanceListPaginator(params).paginate(sorted);
}

ValidationReport checkFieldsForUniqueNess(
Expand Down Expand Up @@ -1007,7 +1011,8 @@ private SqlQuery selectInstancesSql(
List<Object> parameters = new ArrayList<>();

for (FilterBy filterBy : queryParams.toList()) {
if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)) {
if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)
|| PaginationParams.isPaginationParam(filterBy.fieldName)) {
continue;
}
if (!entity.hasFieldNameDefined(filterBy.fieldName)) {
Expand Down Expand Up @@ -1049,6 +1054,7 @@ private SqlQuery selectInstancesSql(
}

appendOrderBy(sql, entity, queryParams, "");
appendPagination(sql, queryParams, parameters);

return new SqlQuery(entity, sql.toString(), parameters);
}
Expand Down Expand Up @@ -1092,7 +1098,8 @@ private SqlQuery selectRelatedInstancesSql(
parameters.add(instance.getInternalId());

for (FilterBy filterBy : queryParams.toList()) {
if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)) {
if (EntityListSortParamParser.isSortByParam(filterBy.fieldName)
|| PaginationParams.isPaginationParam(filterBy.fieldName)) {
continue;
}
if (!connectedEntity.hasFieldNameDefined(filterBy.fieldName)) {
Expand Down Expand Up @@ -1159,6 +1166,26 @@ private void appendOrderBy(
}
}

private void appendPagination(
final StringBuilder sql,
final QueryFilterParams queryParams,
final List<Object> parameters) {
PaginationParams pagination = new PaginationParams(queryParams);
if (pagination.hasValidationError()) {
throw new IllegalArgumentException(pagination.validationError());
}
if (!pagination.hasLimit() && !pagination.hasOffset()) {
return;
}

sql.append(" LIMIT ?");
parameters.add(pagination.limitOr(-1));
if (pagination.hasOffset()) {
sql.append(" OFFSET ?");
parameters.add(pagination.offsetOr(0));
}
}

private String regexSqlCondition(
final String column, final String regex, final List<Object> parameters) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ public void sortByParameterNameIsExactAndCaseSensitive() {
Assertions.assertFalse(SortByFieldName.isSortByParam("sort_by"));
}

@Test
public void paginationParameterNamesAreExactAndBareNamesRemainFilters() {
Assertions.assertTrue(PaginationParams.isPaginationParam("_limit"));
Assertions.assertTrue(PaginationParams.isPaginationParam("_offset"));
Assertions.assertFalse(PaginationParams.isPaginationParam("limit"));
Assertions.assertFalse(PaginationParams.isPaginationParam("offset"));

QueryFilterParams params = new QueryFilterParams();
params.put("limit", "high");
params.put("_limit", "1");

Assertions.assertEquals(1, params.fieldFilters().size());
Assertions.assertEquals("limit", params.fieldFilters().get(0).fieldName);
}

@Test
public void canSortByMultipleFieldsViaAQuery() {
final EntityInstance falseLow =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,21 @@ private void exerciseRepositoryContract(final ThingStore repository) {
Assertions.assertEquals("2", sortedProjects.get(0).getPrimaryKeyValue());
Assertions.assertEquals("1", sortedProjects.get(1).getPrimaryKeyValue());

create(repository, projectDefinition, "Third project");
QueryFilterParams pagedProjectsParams = new QueryFilterParams();
pagedProjectsParams.put("_sortBy", "+id");
pagedProjectsParams.put("_limit", "1");
pagedProjectsParams.put("_offset", "1");
Assertions.assertEquals(
List.of(secondProject),
repository.entityQueries().list(projectDefinition, pagedProjectsParams));

QueryFilterParams emptyPageParams = new QueryFilterParams();
emptyPageParams.put("_limit", "2");
emptyPageParams.put("_offset", "20");
Assertions.assertTrue(
repository.entityQueries().list(projectDefinition, emptyPageParams).isEmpty());

QueryFilterParams regexParams = new QueryFilterParams();
regexParams.put("title", "~=Repository.*");
List<EntityInstance> regexProjects =
Expand Down Expand Up @@ -780,6 +795,14 @@ private void exerciseRepositoryContract(final ThingStore repository) {
Assertions.assertEquals("2", sortedTasks.get(0).getPrimaryKeyValue());
Assertions.assertEquals("1", sortedTasks.get(1).getPrimaryKeyValue());

QueryFilterParams relationshipPageParams = new QueryFilterParams();
relationshipPageParams.put("_sortBy", "+id");
relationshipPageParams.put("_limit", "1");
relationshipPageParams.put("_offset", "1");
Assertions.assertEquals(
List.of(secondTask),
repository.relationships().listRelated(project, "tasks", relationshipPageParams));

repository.relationships().removeBetween(project, task, "tasks");

Assertions.assertTrue(repository.relationships().listRelated(task, "task-of").isEmpty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public void canCreateOpenApi32Swagger() {
Assertions.assertEquals("3.2.0", openApiVersion(swagger));
Assertions.assertTrue(todos.has("query"));
Assertions.assertTrue(hasParameterNamed(todos.getAsJsonObject("get"), "_sortBy"));
Assertions.assertTrue(hasParameterNamed(todos.getAsJsonObject("get"), "_limit"));
Assertions.assertTrue(hasParameterNamed(todos.getAsJsonObject("get"), "_offset"));
Assertions.assertTrue(hasParameterNamed(query, "_sortBy"));
Assertions.assertTrue(hasParameterNamed(query, "_limit"));
Assertions.assertTrue(hasParameterNamed(query, "_offset"));
Assertions.assertFalse(swagger.contains("\"x-query-operation\""));
Assertions.assertFalse(swagger.contains("\"x-http-method\""));
Assertions.assertFalse(swagger.contains("\"x-query-content-types\""));
Expand Down
Loading
Loading