Skip to content

Commit

Permalink
Getting Katharsis to adhere to the Json api specification for query
Browse files Browse the repository at this point in the history
parameters katharsis-project#4

Simplify/enhance the QueryParamsParser interface by utilizing a context object
to supply the parser with the raw query parameter information from the request, as
well as a reference to the ResourceInformation of the requested resource.

Still todo:

-Implement jsonapi-compliant parser
-Add unit tests for parsers/contexts
-Remove QueryParamsBuilder
-Update existing unit tests
-Test servlet/jax-rs implementations
  • Loading branch information
dustinstanley committed Sep 5, 2016
1 parent 938ef3d commit 1772023
Show file tree
Hide file tree
Showing 16 changed files with 621 additions and 400 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,277 @@

package io.katharsis.queryParams;

import io.katharsis.jackson.exception.ParametersDeserializationException;
import io.katharsis.queryParams.context.QueryParamsParserContext;
import io.katharsis.queryParams.include.Inclusion;
import io.katharsis.queryParams.params.*;
import io.katharsis.resource.RestrictedQueryParamsMembers;
import io.katharsis.utils.StringUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
*
*/
public class DefaultQueryParamsParser implements QueryParamsParser {

@Override
public Map<String, Set<String>> parseFiltersParameters(final Map<String, Set<String>> queryParams) {
protected TypedParams<FilterParams> parseFiltersParameters(final QueryParamsParserContext context) {
String filterKey = RestrictedQueryParamsMembers.filter.name();
return filterQueryParamsByKey(queryParams, filterKey);
Map<String, Set<String>> filters = filterQueryParamsByKey(context, filterKey);

Map<String, Map<String, Set<String>>> temporaryFiltersMap = new LinkedHashMap<>();

for (Map.Entry<String, Set<String>> entry : filters.entrySet()) {

List<String> propertyList = buildPropertyListFromEntry(entry, filterKey);

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));
}

return new TypedParams<>(Collections.unmodifiableMap(decodedFiltersMap));
}

@Override
public Map<String, Set<String>> parseSortingParameters(final Map<String, Set<String>> queryParams) {
protected TypedParams<SortingParams> parseSortingParameters(final QueryParamsParserContext context) {
String sortingKey = RestrictedQueryParamsMembers.sort.name();
return filterQueryParamsByKey(queryParams, sortingKey);
Map<String, Set<String>> sorting = filterQueryParamsByKey(context, sortingKey);

Map<String, Map<String, RestrictedSortingValues>> temporarySortingMap = new LinkedHashMap<>();

for (Map.Entry<String, Set<String>> entry : sorting.entrySet()) {

List<String> propertyList = buildPropertyListFromEntry(entry, sortingKey);

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));
}

return new TypedParams<>(Collections.unmodifiableMap(decodedSortingMap));
}

@Override
public Map<String, Set<String>> parseGroupingParameters(final Map<String, Set<String>> queryParams) {
protected TypedParams<GroupingParams> parseGroupingParameters(final QueryParamsParserContext context) {
String groupingKey = RestrictedQueryParamsMembers.group.name();
return filterQueryParamsByKey(queryParams, groupingKey);
Map<String, Set<String>> grouping = filterQueryParamsByKey(context, groupingKey);

Map<String, Set<String>> temporaryGroupingMap = new LinkedHashMap<>();

for (Map.Entry<String, Set<String>> entry : grouping.entrySet()) {

List<String> propertyList = buildPropertyListFromEntry(entry, groupingKey);

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));
}

return new TypedParams<>(Collections.unmodifiableMap(decodedGroupingMap));
}

@Override
public Map<String, Set<String>> parseIncludedFieldsParameters(final Map<String, Set<String>> queryParams) {
protected TypedParams<IncludedFieldsParams> parseIncludedFieldsParameters(final QueryParamsParserContext context) {
String sparseKey = RestrictedQueryParamsMembers.fields.name();
return filterQueryParamsByKey(queryParams, sparseKey);
Map<String, Set<String>> sparse = filterQueryParamsByKey(context, sparseKey);

Map<String, Set<String>> temporarySparseMap = new LinkedHashMap<>();

for (Map.Entry<String, Set<String>> entry : sparse.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, sparseKey);

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));
}

return new TypedParams<>(Collections.unmodifiableMap(decodedSparseMap));
}

@Override
public Map<String, Set<String>> parseIncludedRelationsParameters(final Map<String, Set<String>> queryParams) {
protected TypedParams<IncludedRelationsParams> parseIncludedRelationsParameters(QueryParamsParserContext context) {
String includeKey = RestrictedQueryParamsMembers.include.name();
return filterQueryParamsByKey(queryParams, includeKey);
Map<String, Set<String>> inclusions = filterQueryParamsByKey(context, includeKey);

Map<String, Set<Inclusion>> temporaryInclusionsMap = new LinkedHashMap<>();

for (Map.Entry<String, Set<String>> entry : inclusions.entrySet()) {
List<String> propertyList = buildPropertyListFromEntry(entry, includeKey);

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));
}

return new TypedParams<>(Collections.unmodifiableMap(decodedInclusions));
}

@Override
public Map<String, Set<String>> parsePaginationParameters(final Map<String, Set<String>> queryParams) {
protected Map<RestrictedPaginationKeys, Integer> parsePaginationParameters(final QueryParamsParserContext context) {
String pagingKey = RestrictedQueryParamsMembers.page.name();
return filterQueryParamsByKey(queryParams, pagingKey);
Map<String, Set<String>> pagination = filterQueryParamsByKey(context, pagingKey);

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()));
}

return Collections.unmodifiableMap(decodedPagination);
}

/**
* Filters provided query params to one starting with provided string key
*
* @param queryParams Request query params
* @param context used to inspect the parameters of the current request
* @param queryKey Filtering key
* @return Filtered query params
*/
private static Map<String, Set<String>> filterQueryParamsByKey(Map<String, Set<String>> queryParams, String queryKey) {
protected static Map<String, Set<String>> filterQueryParamsByKey(QueryParamsParserContext context, String queryKey) {
Map<String, Set<String>> filteredQueryParams = new HashMap<>();

for (Map.Entry<String, Set<String>> entry : queryParams.entrySet()) {
if (entry.getKey().startsWith(queryKey)) {
filteredQueryParams.put(entry.getKey(), entry.getValue());
for (String paramName : context.getParameterNames()) {
if (paramName.startsWith(queryKey)) {
filteredQueryParams.put(paramName, context.getParameterValue(paramName));
}
}
return filteredQueryParams;
}

protected 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 QueryParams parse(QueryParamsParserContext context) {
QueryParams queryParams = new QueryParams();
queryParams.setFilters(parseFiltersParameters(context));
queryParams.setSorting(parseSortingParameters(context));
queryParams.setGrouping(parseGroupingParameters(context));
queryParams.setPagination(parsePaginationParameters(context));
queryParams.setIncludedFields(parseIncludedFieldsParameters(context));
queryParams.setIncludedRelations(parseIncludedRelationsParameters(context));
return queryParams;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.katharsis.queryParams;

import io.katharsis.queryParams.context.QueryParamsParserContext;
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 java.util.Collections;

/**
*
*/
public class JsonApiQueryParamsParser extends DefaultQueryParamsParser {

//TODO: implement methods which adhere to official json-api spec
protected TypedParams<SortingParams> parseSortingParameters(final QueryParamsParserContext context) {
throw new UnsupportedOperationException("Not implemented");
}

protected TypedParams<IncludedFieldsParams> parseIncludedFieldsParameters(final QueryParamsParserContext context) {
throw new UnsupportedOperationException("Not implemented");
}

protected TypedParams<IncludedRelationsParams> parseIncludedRelationsParameters(QueryParamsParserContext context) {
throw new UnsupportedOperationException("Not implemented");
}
}
Loading

0 comments on commit 1772023

Please sign in to comment.