package io.katharsis.queryParams;
import io.katharsis.jackson.exception.ParametersDeserializationException;
import io.katharsis.queryParams.include.Inclusion;
import io.katharsis.queryParams.params.FilterParams;
import io.katharsis.queryParams.params.GroupingParams;
import io.katharsis.queryParams.params.IncludedFieldsParams;
import io.katharsis.queryParams.params.IncludedRelationsParams;
import io.katharsis.queryParams.params.SortingParams;
import io.katharsis.queryParams.params.TypedParams;
import io.katharsis.resource.RestrictedQueryParamsMembers;
import io.katharsis.utils.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains a set of parameters passed along with the request.
*/
public class QueryParams {
private TypedParams<FilterParams> filters;
private TypedParams<SortingParams> sorting;
private TypedParams<GroupingParams> grouping;
private TypedParams<IncludedFieldsParams> includedFields;
private TypedParams<IncludedRelationsParams> includedRelations;
private Map<RestrictedPaginationKeys, Integer> pagination;
/**
* <strong>Important!</strong> Katharsis implementation differs form JSON API
* <a href="http://jsonapi.org/format/#fetching-filtering">definition of filtering</a>
* in order to fit standard query parameter serializing strategy and maximize effective processing of data.
* <p>
* Filter params can be send with following format (Katharsis does not specify or implement any operators): <br>
* <strong>filter[ResourceType][property|operator]([property|operator])* = "value"</strong><br>
* <p>
* Examples of accepted filtering of resources:
* <ul>
* <li>{@code GET /tasks/?filter[tasks][name]=Super task}</li>
* <li>{@code GET /tasks/?filter[tasks][name]=Super task&filter[tasks][dueDate]=2015-10-01}</li>
* <li>{@code GET /tasks/?filter[tasks][name][$startWith]=Super task}</li>
* <li>{@code GET /tasks/?filter[tasks][name][][$startWith]=Super&filter[tasks][name][][$endWith]=task}</li>
* </ul>
*
* @return {@link TypedParams} Map of filtering params passed to a request grouped by type of resource
*/
public TypedParams<FilterParams> getFilters() {
return filters;
}
void setFilters(Map<String, Set<String>> filters) {
Map<String, Map<String, Set<String>>> temporaryFiltersMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : filters.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.filter.name());
String resourceType = propertyList.get(0);
String propertyPath = StringUtils.join(".", propertyList.subList(1, propertyList.size()));
if (temporaryFiltersMap.containsKey(resourceType)) {
Map<String, Set<String>> resourceParams = temporaryFiltersMap.get(resourceType);
resourceParams.put(propertyPath, Collections.unmodifiableSet(entry.getValue()));
} else {
Map<String, Set<String>> resourceParams = new LinkedHashMap<>();
temporaryFiltersMap.put(resourceType, resourceParams);
resourceParams.put(propertyPath, entry.getValue());
}
}
Map<String, FilterParams> decodedFiltersMap = new LinkedHashMap<>();
for (Map.Entry<String, Map<String, Set<String>>> resourceTypesMap : temporaryFiltersMap.entrySet()) {
Map<String, Set<String>> filtersMap = Collections.unmodifiableMap(resourceTypesMap.getValue());
decodedFiltersMap.put(resourceTypesMap.getKey(), new FilterParams(filtersMap));
}
this.filters = new TypedParams<>(Collections.unmodifiableMap(decodedFiltersMap));
}
/**
* <strong>Important!</strong> Katharsis implementation differs form JSON API
* <a href="http://jsonapi.org/format/#fetching-sorting">definition of sorting</a>
* in order to fit standard query parameter serializing strategy and maximize effective processing of data.
* <p>
* Sort params can be send with following format: <br>
* <strong>sort[ResourceType][property]([property])* = "asc|desc"</strong>
* <p>
* Examples of accepted sorting of resources:
* <ul>
* <li>{@code GET /tasks/?sort[tasks][name]=asc}</li>
* <li>{@code GET /project/?sort[projects][shortName]=desc&sort[users][name][firstName]=asc}</li>
* </ul>
*
* @return {@link TypedParams} Map of sorting params passed to request grouped by type of resource
*/
public TypedParams<SortingParams> getSorting() {
return sorting;
}
void setSorting(Map<String, Set<String>> sorting) {
Map<String, Map<String, RestrictedSortingValues>> temporarySortingMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : sorting.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.sort.name());
String resourceType = propertyList.get(0);
String propertyPath = StringUtils.join(".", propertyList.subList(1, propertyList.size()));
if (temporarySortingMap.containsKey(resourceType)) {
Map<String, RestrictedSortingValues> resourceParams = temporarySortingMap.get(resourceType);
resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue()
.iterator()
.next()));
} else {
Map<String, RestrictedSortingValues> resourceParams = new HashMap<>();
temporarySortingMap.put(resourceType, resourceParams);
resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue()
.iterator()
.next()));
}
}
Map<String, SortingParams> decodedSortingMap = new LinkedHashMap<>();
for (Map.Entry<String, Map<String, RestrictedSortingValues>> resourceTypesMap : temporarySortingMap.entrySet
()) {
Map<String, RestrictedSortingValues> sortingMap = Collections.unmodifiableMap(resourceTypesMap.getValue());
decodedSortingMap.put(resourceTypesMap.getKey(), new SortingParams(sortingMap));
}
this.sorting = new TypedParams<>(Collections.unmodifiableMap(decodedSortingMap));
}
/**
* <strong>Important: </strong> Grouping itself is not specified by JSON API itself, but the
* keyword and format it reserved for today and future use in Katharsis.
* <p>
* Group params can be send with following format: <br>
* <strong>group[ResourceType] = "property(.property)*"</strong>
* <p>
* Examples of accepted grouping of resources:
* <ul>
* <li>{@code GET /tasks/?group[tasks]=name}</li>
* <li>{@code GET /project/?group[users]=name.firstName&include[projects]=team}</li>
* </ul>
*
* @return {@link Map} Map of grouping params passed to request grouped by type of resource
*/
public TypedParams<GroupingParams> getGrouping() {
return grouping;
}
void setGrouping(Map<String, Set<String>> grouping) {
Map<String, Set<String>> temporaryGroupingMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : grouping.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.group.name());
if (propertyList.size() > 1) {
throw new ParametersDeserializationException("Exceeded maximum level of nesting of 'group' parameter " +
"(1) eg. group[tasks][name] <-- #2 level and more are not allowed");
}
String resourceType = propertyList.get(0);
if (temporaryGroupingMap.containsKey(resourceType)) {
Set<String> resourceParams = temporaryGroupingMap.get(resourceType);
resourceParams.addAll(entry.getValue());
temporaryGroupingMap.put(resourceType, resourceParams);
} else {
Set<String> resourceParams = new LinkedHashSet<>();
resourceParams.addAll(entry.getValue());
temporaryGroupingMap.put(resourceType, resourceParams);
}
}
Map<String, GroupingParams> decodedGroupingMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> resourceTypesMap : temporaryGroupingMap.entrySet()) {
Set<String> groupingSet = Collections.unmodifiableSet(resourceTypesMap.getValue());
decodedGroupingMap.put(resourceTypesMap.getKey(), new GroupingParams(groupingSet));
}
this.grouping = new TypedParams<>(Collections.unmodifiableMap(decodedGroupingMap));
}
/**
* <strong>Important!</strong> Katharsis implementation sets on strategy of pagination whereas JSON API
* <a href="http://jsonapi.org/format/#fetching-pagination">definition of pagination</a>
* is agnostic about pagination strategies.
* <p>
* Pagination params can be send with following format: <br>
* <strong>page[offset|limit] = "value"</strong>, where value is an integer
* <p>
* Examples of accepted grouping of resources:
* <ul>
* <li>{@code GET /projects/?page[offset]=0&page[limit]=10}</li>
* </ul>
*
* @return {@link Map} Map of pagination keys passed to request
*/
public Map<RestrictedPaginationKeys, Integer> getPagination() {
return pagination;
}
void setPagination(Map<String, Set<String>> pagination) {
Map<RestrictedPaginationKeys, Integer> decodedPagination = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : pagination.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.page.name());
if (propertyList.size() > 1) {
throw new ParametersDeserializationException("Exceeded maximum level of nesting of 'page' parameter " +
"(1) eg. page[offset][minimal] <-- #2 level and more are not allowed");
}
String resourceType = propertyList.get(0);
decodedPagination.put(RestrictedPaginationKeys.valueOf(resourceType), Integer.parseInt(entry
.getValue()
.iterator()
.next()));
}
this.pagination = Collections.unmodifiableMap(decodedPagination);
}
/**
* <strong>Important!</strong> Katharsis implementation differs form JSON API
* <a href="http://jsonapi.org/format/#fetching-sparse-fieldsets">definition of sparse field set</a>
* in order to fit standard query parameter serializing strategy and maximize effective processing of data.
* <p>
* Sparse field set params can be send with following format: <br>
* <strong>fields[ResourceType] = "property(.property)*"</strong><br>
* <p>
* Examples of accepted sparse field sets of resources:
* <ul>
* <li>{@code GET /tasks/?fields[tasks]=name}</li>
* <li>{@code GET /tasks/?fields[tasks][]=name&fields[tasks][]=dueDate}</li>
* <li>{@code GET /tasks/?fields[users]=name.surname&include[tasks]=author}</li>
* </ul>
*
* @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource
*/
public TypedParams<IncludedFieldsParams> getIncludedFields() {
return includedFields;
}
void setIncludedFields(Map<String, Set<String>> sparse) {
Map<String, Set<String>> temporarySparseMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : sparse.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.fields.name());
if (propertyList.size() > 1) {
throw new ParametersDeserializationException("Exceeded maximum level of nesting of 'fields' " +
"parameter (1) eg. fields[tasks][name] <-- #2 level and more are not allowed");
}
String resourceType = propertyList.get(0);
if (temporarySparseMap.containsKey(resourceType)) {
Set<String> resourceParams = temporarySparseMap.get(resourceType);
resourceParams.addAll(entry.getValue());
temporarySparseMap.put(resourceType, resourceParams);
} else {
Set<String> resourceParams = new LinkedHashSet<>();
resourceParams.addAll(entry.getValue());
temporarySparseMap.put(resourceType, resourceParams);
}
}
Map<String, IncludedFieldsParams> decodedSparseMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> resourceTypesMap : temporarySparseMap.entrySet()) {
Set<String> sparseSet = Collections.unmodifiableSet(resourceTypesMap.getValue());
decodedSparseMap.put(resourceTypesMap.getKey(), new IncludedFieldsParams(sparseSet));
}
this.includedFields = new TypedParams<>(Collections.unmodifiableMap(decodedSparseMap));
}
/**
* <strong>Important!</strong> Katharsis implementation differs form JSON API
* <a href="http://jsonapi.org/format/#fetching-includes">definition of includes</a>
* in order to fit standard query parameter serializing strategy and maximize effective processing of data.
* <p>
* Included field set params can be send with following format: <br>
* <strong>include[ResourceType] = "property(.property)*"</strong><br>
* <p>
* Examples of accepted sparse field sets of resources:
* <ul>
* <li>{@code GET /tasks/?include[tasks]=author}</li>
* <li>{@code GET /tasks/?include[tasks][]=author&include[tasks][]=comments}</li>
* <li>{@code GET /projects/?include[projects]=task&include[tasks]=comments}</li>
* </ul>
*
* @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource
*/
public TypedParams<IncludedRelationsParams> getIncludedRelations() {
return includedRelations;
}
void setIncludedRelations(Map<String, Set<String>> inclusions) {
Map<String, Set<Inclusion>> temporaryInclusionsMap = new LinkedHashMap<>();
for (Map.Entry<String, Set<String>> entry : inclusions.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, RestrictedQueryParamsMembers.include.name());
if (propertyList.size() > 1) {
throw new ParametersDeserializationException("Exceeded maximum level of nesting of 'include' " +
"parameter (1)");
}
String resourceType = propertyList.get(0);
Set<Inclusion> resourceParams;
if (temporaryInclusionsMap.containsKey(resourceType)) {
resourceParams = temporaryInclusionsMap.get(resourceType);
} else {
resourceParams = new LinkedHashSet<>();
}
for(String path : entry.getValue()) {
resourceParams.add(new Inclusion(path));
}
temporaryInclusionsMap.put(resourceType, resourceParams);
}
Map<String, IncludedRelationsParams> decodedInclusions = new LinkedHashMap<>();
for (Map.Entry<String, Set<Inclusion>> resourceTypesMap : temporaryInclusionsMap.entrySet()) {
Set<Inclusion> inclusionSet = Collections.unmodifiableSet(resourceTypesMap.getValue());
decodedInclusions.put(resourceTypesMap.getKey(), new IncludedRelationsParams(inclusionSet));
}
this.includedRelations = new TypedParams<>(Collections.unmodifiableMap(decodedInclusions));
}
private static List<String> buildPropertyListFromEntry(Map.Entry<String, Set<String>> entry, String prefix) {
String entryKey = entry.getKey()
.substring(prefix.length());
String pattern = "[^\\]\\[]+(?<!\\[)(?=\\])";
Pattern regexp = Pattern.compile(pattern);
Matcher matcher = regexp.matcher(entryKey);
List<String> matchList = new LinkedList<>();
while (matcher.find()) {
matchList.add(matcher.group());
}
if (matchList.isEmpty()) {
throw new ParametersDeserializationException("Malformed filter parameter: " + entryKey);
}
return matchList;
}
@Override
public String toString() {
return "QueryParams{" +
"filters=" + filters +
", sorting=" + sorting +
", grouping=" + grouping +
", includedFields=" + includedFields +
", includedRelations=" + includedRelations +
", pagination=" + pagination +
'}';
}
}