diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/DefaultQueryParamsParser.java b/katharsis-core/src/main/java/io/katharsis/queryParams/DefaultQueryParamsParser.java index 777309be..278fa06a 100644 --- a/katharsis-core/src/main/java/io/katharsis/queryParams/DefaultQueryParamsParser.java +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/DefaultQueryParamsParser.java @@ -17,65 +17,377 @@ 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; +/** + * The default QueryParamsParser implementation which parses query parameters with the behavior + * specified in the Katharsis documentation. This parser does NOT adhere to the JSON-API specification, + * but it does provide more flexibility. If you need to adhere to the spec, use {@link JsonApiQueryParamsParser} + */ public class DefaultQueryParamsParser implements QueryParamsParser { - @Override - public Map> parseFiltersParameters(final Map> queryParams) { + /** + * Important! Katharsis implementation differs form JSON API + * definition of filtering + * in order to fit standard query parameter serializing strategy and maximize effective processing of data. + *

+ * Filter params can be send with following format (Katharsis does not specify or implement any operators):
+ * filter[ResourceType][property|operator]([property|operator])* = "value"
+ *

+ * Examples of accepted filtering of resources: + *

+ * + * @return {@link TypedParams} Map of filtering params passed to a request grouped by type of resource + */ + protected TypedParams parseFiltersParameters(final QueryParamsParserContext context) { String filterKey = RestrictedQueryParamsMembers.filter.name(); - return filterQueryParamsByKey(queryParams, filterKey); + Map> filters = filterQueryParamsByKey(context, filterKey); + + Map>> temporaryFiltersMap = new LinkedHashMap<>(); + + for (Map.Entry> entry : filters.entrySet()) { + + List propertyList = buildPropertyListFromEntry(entry, filterKey); + + String resourceType = propertyList.get(0); + String propertyPath = StringUtils.join(".", propertyList.subList(1, propertyList.size())); + + if (temporaryFiltersMap.containsKey(resourceType)) { + Map> resourceParams = temporaryFiltersMap.get(resourceType); + resourceParams.put(propertyPath, Collections.unmodifiableSet(entry.getValue())); + } else { + Map> resourceParams = new LinkedHashMap<>(); + temporaryFiltersMap.put(resourceType, resourceParams); + resourceParams.put(propertyPath, entry.getValue()); + } + } + + Map decodedFiltersMap = new LinkedHashMap<>(); + + for (Map.Entry>> resourceTypesMap : temporaryFiltersMap.entrySet()) { + Map> filtersMap = Collections.unmodifiableMap(resourceTypesMap.getValue()); + decodedFiltersMap.put(resourceTypesMap.getKey(), new FilterParams(filtersMap)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedFiltersMap)); } - @Override - public Map> parseSortingParameters(final Map> queryParams) { + /** + * Important! Katharsis implementation differs form JSON API + * definition of sorting + * in order to fit standard query parameter serializing strategy and maximize effective processing of data. + *

+ * Sort params can be send with following format:
+ * sort[ResourceType][property]([property])* = "asc|desc" + *

+ * Examples of accepted sorting of resources: + *

    + *
  • {@code GET /tasks/?sort[tasks][name]=asc}
  • + *
  • {@code GET /project/?sort[projects][shortName]=desc&sort[users][name][firstName]=asc}
  • + *
+ * + * @return {@link TypedParams} Map of sorting params passed to request grouped by type of resource + */ + protected TypedParams parseSortingParameters(final QueryParamsParserContext context) { String sortingKey = RestrictedQueryParamsMembers.sort.name(); - return filterQueryParamsByKey(queryParams, sortingKey); + Map> sorting = filterQueryParamsByKey(context, sortingKey); + + Map> temporarySortingMap = new LinkedHashMap<>(); + + for (Map.Entry> entry : sorting.entrySet()) { + + List propertyList = buildPropertyListFromEntry(entry, sortingKey); + + String resourceType = propertyList.get(0); + String propertyPath = StringUtils.join(".", propertyList.subList(1, propertyList.size())); + + + if (temporarySortingMap.containsKey(resourceType)) { + Map resourceParams = temporarySortingMap.get(resourceType); + resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue() + .iterator() + .next())); + } else { + Map resourceParams = new HashMap<>(); + temporarySortingMap.put(resourceType, resourceParams); + resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue() + .iterator() + .next())); + } + } + + Map decodedSortingMap = new LinkedHashMap<>(); + + for (Map.Entry> resourceTypesMap : temporarySortingMap.entrySet()) { + Map sortingMap = Collections.unmodifiableMap(resourceTypesMap.getValue()); + decodedSortingMap.put(resourceTypesMap.getKey(), new SortingParams(sortingMap)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedSortingMap)); } - @Override - public Map> parseGroupingParameters(final Map> queryParams) { + /** + * Important: Grouping itself is not specified by JSON API itself, but the + * keyword and format it reserved for today and future use in Katharsis. + *

+ * Group params can be send with following format:
+ * group[ResourceType] = "property(.property)*" + *

+ * Examples of accepted grouping of resources: + *

    + *
  • {@code GET /tasks/?group[tasks]=name}
  • + *
  • {@code GET /project/?group[users]=name.firstName&include[projects]=team}
  • + *
+ * + * @return {@link Map} Map of grouping params passed to request grouped by type of resource + */ + protected TypedParams parseGroupingParameters(final QueryParamsParserContext context) { String groupingKey = RestrictedQueryParamsMembers.group.name(); - return filterQueryParamsByKey(queryParams, groupingKey); + Map> grouping = filterQueryParamsByKey(context, groupingKey); + + Map> temporaryGroupingMap = new LinkedHashMap<>(); + + for (Map.Entry> entry : grouping.entrySet()) { + + List 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 resourceParams = temporaryGroupingMap.get(resourceType); + resourceParams.addAll(entry.getValue()); + temporaryGroupingMap.put(resourceType, resourceParams); + } else { + Set resourceParams = new LinkedHashSet<>(); + resourceParams.addAll(entry.getValue()); + temporaryGroupingMap.put(resourceType, resourceParams); + } + } + + Map decodedGroupingMap = new LinkedHashMap<>(); + + for (Map.Entry> resourceTypesMap : temporaryGroupingMap.entrySet()) { + Set groupingSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); + decodedGroupingMap.put(resourceTypesMap.getKey(), new GroupingParams(groupingSet)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedGroupingMap)); } - @Override - public Map> parseIncludedFieldsParameters(final Map> queryParams) { + /** + * Important! Katharsis implementation differs form JSON API + * definition of sparse field set + * in order to fit standard query parameter serializing strategy and maximize effective processing of data. + *

+ * Sparse field set params can be send with following format:
+ * fields[ResourceType] = "property(.property)*"
+ *

+ * Examples of accepted sparse field sets of resources: + *

    + *
  • {@code GET /tasks/?fields[tasks]=name}
  • + *
  • {@code GET /tasks/?fields[tasks][]=name&fields[tasks][]=dueDate}
  • + *
  • {@code GET /tasks/?fields[users]=name.surname&include[tasks]=author}
  • + *
+ * + * @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource + */ + protected TypedParams parseIncludedFieldsParameters(final QueryParamsParserContext context) { String sparseKey = RestrictedQueryParamsMembers.fields.name(); - return filterQueryParamsByKey(queryParams, sparseKey); + Map> sparse = filterQueryParamsByKey(context, sparseKey); + + Map> temporarySparseMap = new LinkedHashMap<>(); + + for (Map.Entry> entry : sparse.entrySet()) { + List 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 resourceParams = temporarySparseMap.get(resourceType); + resourceParams.addAll(entry.getValue()); + temporarySparseMap.put(resourceType, resourceParams); + } else { + Set resourceParams = new LinkedHashSet<>(); + resourceParams.addAll(entry.getValue()); + temporarySparseMap.put(resourceType, resourceParams); + } + } + + Map decodedSparseMap = new LinkedHashMap<>(); + + for (Map.Entry> resourceTypesMap : temporarySparseMap.entrySet()) { + Set sparseSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); + decodedSparseMap.put(resourceTypesMap.getKey(), new IncludedFieldsParams(sparseSet)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedSparseMap)); } - @Override - public Map> parseIncludedRelationsParameters(final Map> queryParams) { + /** + * Important! Katharsis implementation differs form JSON API + * definition of includes + * in order to fit standard query parameter serializing strategy and maximize effective processing of data. + *

+ * Included field set params can be send with following format:
+ * include[ResourceType] = "property(.property)*"
+ *

+ * Examples of accepted sparse field sets of resources: + *

    + *
  • {@code GET /tasks/?include[tasks]=author}
  • + *
  • {@code GET /tasks/?include[tasks][]=author&include[tasks][]=comments}
  • + *
  • {@code GET /projects/?include[projects]=task&include[tasks]=comments}
  • + *
+ * + * @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource + */ + protected TypedParams parseIncludedRelationsParameters(QueryParamsParserContext context) { String includeKey = RestrictedQueryParamsMembers.include.name(); - return filterQueryParamsByKey(queryParams, includeKey); + Map> inclusions = filterQueryParamsByKey(context, includeKey); + + Map> temporaryInclusionsMap = new LinkedHashMap<>(); + + for (Map.Entry> entry : inclusions.entrySet()) { + List 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 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 decodedInclusions = new LinkedHashMap<>(); + + for (Map.Entry> resourceTypesMap : temporaryInclusionsMap.entrySet()) { + Set inclusionSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); + decodedInclusions.put(resourceTypesMap.getKey(), new IncludedRelationsParams(inclusionSet)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedInclusions)); } - @Override - public Map> parsePaginationParameters(final Map> queryParams) { + /** + * Important! Katharsis implementation sets on strategy of pagination whereas JSON API + * definition of pagination + * is agnostic about pagination strategies. + *

+ * Pagination params can be send with following format:
+ * page[offset|limit] = "value", where value is an integer + *

+ * Examples of accepted grouping of resources: + *

    + *
  • {@code GET /projects/?page[offset]=0&page[limit]=10}
  • + *
+ * + * @return {@link Map} Map of pagination keys passed to request + */ + protected Map parsePaginationParameters(final QueryParamsParserContext context) { String pagingKey = RestrictedQueryParamsMembers.page.name(); - return filterQueryParamsByKey(queryParams, pagingKey); + Map> pagination = filterQueryParamsByKey(context, pagingKey); + + Map decodedPagination = new LinkedHashMap<>(); + + for (Map.Entry> entry : pagination.entrySet()) { + List 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> filterQueryParamsByKey(Map> queryParams, String queryKey) { + protected Map> filterQueryParamsByKey(QueryParamsParserContext context, String queryKey) { Map> filteredQueryParams = new HashMap<>(); - for (Map.Entry> 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 buildPropertyListFromEntry(Map.Entry> entry, String prefix) { + String entryKey = entry.getKey() + .substring(prefix.length()); + + String pattern = "[^\\]\\[]+(? 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; + } } diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/JsonApiQueryParamsParser.java b/katharsis-core/src/main/java/io/katharsis/queryParams/JsonApiQueryParamsParser.java new file mode 100644 index 00000000..82115fbb --- /dev/null +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/JsonApiQueryParamsParser.java @@ -0,0 +1,97 @@ +package io.katharsis.queryParams; + +import io.katharsis.queryParams.context.QueryParamsParserContext; +import io.katharsis.queryParams.include.Inclusion; +import io.katharsis.queryParams.params.IncludedRelationsParams; +import io.katharsis.queryParams.params.SortingParams; +import io.katharsis.queryParams.params.TypedParams; +import io.katharsis.resource.RestrictedQueryParamsMembers; + +import java.util.*; + +/** + * A {@link QueryParamsParser} implementation which adheres to the JSON-API standard more strictly + * than the DefaultQueryParamsParser, at the expense of having less flexibility. + */ +public class JsonApiQueryParamsParser extends DefaultQueryParamsParser { + + private static final String JSON_API_SORT_INDICATOR_DESC = "-"; + private static final String JSON_API_PARAM_DELIMITER = ","; + + protected TypedParams parseSortingParameters(final QueryParamsParserContext context) { + String sortingKey = RestrictedQueryParamsMembers.sort.name(); + Set rawSortingQueryParams = parseDelimitedParameters(context.getParameterValue(sortingKey)); + Map decodedSortingMap = new LinkedHashMap<>(); + + if (!rawSortingQueryParams.isEmpty()) { + Map temporarySortingMap = new LinkedHashMap<>(); + + for (String sortParam : rawSortingQueryParams) { + if (sortParam.startsWith(JSON_API_SORT_INDICATOR_DESC)) { + temporarySortingMap.put(sortParam.substring(1), RestrictedSortingValues.desc); + } else { + temporarySortingMap.put(sortParam, RestrictedSortingValues.asc); + } + } + + decodedSortingMap.put(context.getRequestedResourceInformation().getResourceType(), + new SortingParams(temporarySortingMap)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedSortingMap)); + } + + protected TypedParams parseIncludedRelationsParameters(QueryParamsParserContext context) { + String includeKey = RestrictedQueryParamsMembers.include.name(); + Map> inclusions = filterQueryParamsByKey(context, includeKey); + + Map decodedInclusions = new LinkedHashMap<>(); + + if (inclusions.containsKey(RestrictedQueryParamsMembers.include.name())) { + Set inclusionSet = new LinkedHashSet<>(); + for (String inclusion : inclusions.get(RestrictedQueryParamsMembers.include.name())) { + inclusionSet.add(new Inclusion(inclusion)); + } + decodedInclusions.put(context.getRequestedResourceInformation().getResourceType(), + new IncludedRelationsParams(inclusionSet)); + } + + return new TypedParams<>(Collections.unmodifiableMap(decodedInclusions)); + } + + /** + * Returns a list of all of the strings contained in parametersToParse. If any of the + * strings contained in parametersToParse is a comma-delimited list, that string will be split + * into substrings and each substring will be added to the returned set (in place of the + * delimited list). + */ + private static Set parseDelimitedParameters(Set parametersToParse) { + Set parsedParameters = new LinkedHashSet<>(); + if (parametersToParse != null && !parametersToParse.isEmpty()) { + for (String parameterToParse : parametersToParse) { + parsedParameters.addAll(Arrays.asList(parameterToParse.split(JSON_API_PARAM_DELIMITER))); + } + } + return parsedParameters; + } + + /** + * Filters provided query params to one starting with provided string key. This override also splits param + * values if they are contained in a comma-delimited list. + * + * @param context used to inspect the parameters of the current request + * @param queryKey Filtering key + * @return Filtered query params + */ + @Override + protected Map> filterQueryParamsByKey(QueryParamsParserContext context, String queryKey) { + Map> filteredQueryParams = new HashMap<>(); + + for (String paramName : context.getParameterNames()) { + if (paramName.startsWith(queryKey)) { + filteredQueryParams.put(paramName, parseDelimitedParameters(context.getParameterValue(paramName))); + } + } + return filteredQueryParams; + } +} \ No newline at end of file diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParams.java b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParams.java index 9f3745b8..e721433e 100644 --- a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParams.java +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParams.java @@ -1,26 +1,8 @@ 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 io.katharsis.queryParams.params.*; -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. @@ -33,342 +15,46 @@ public class QueryParams { private TypedParams includedRelations; private Map pagination; - - /** - * Important! Katharsis implementation differs form JSON API - * definition of filtering - * in order to fit standard query parameter serializing strategy and maximize effective processing of data. - *

- * Filter params can be send with following format (Katharsis does not specify or implement any operators):
- * filter[ResourceType][property|operator]([property|operator])* = "value"
- *

- * Examples of accepted filtering of resources: - *

    - *
  • {@code GET /tasks/?filter[tasks][name]=Super task}
  • - *
  • {@code GET /tasks/?filter[tasks][name]=Super task&filter[tasks][dueDate]=2015-10-01}
  • - *
  • {@code GET /tasks/?filter[tasks][name][$startWith]=Super task}
  • - *
  • {@code GET /tasks/?filter[tasks][name][][$startWith]=Super&filter[tasks][name][][$endWith]=task}
  • - *
- * - * @return {@link TypedParams} Map of filtering params passed to a request grouped by type of resource - */ public TypedParams getFilters() { return filters; } - - void setFilters(Map> filters) { - Map>> temporaryFiltersMap = new LinkedHashMap<>(); - - for (Map.Entry> entry : filters.entrySet()) { - - List 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> resourceParams = temporaryFiltersMap.get(resourceType); - resourceParams.put(propertyPath, Collections.unmodifiableSet(entry.getValue())); - } else { - Map> resourceParams = new LinkedHashMap<>(); - temporaryFiltersMap.put(resourceType, resourceParams); - resourceParams.put(propertyPath, entry.getValue()); - } - } - - Map decodedFiltersMap = new LinkedHashMap<>(); - - for (Map.Entry>> resourceTypesMap : temporaryFiltersMap.entrySet()) { - Map> filtersMap = Collections.unmodifiableMap(resourceTypesMap.getValue()); - decodedFiltersMap.put(resourceTypesMap.getKey(), new FilterParams(filtersMap)); - } - - this.filters = new TypedParams<>(Collections.unmodifiableMap(decodedFiltersMap)); + void setFilters(TypedParams filters) { + this.filters = filters; } - /** - * Important! Katharsis implementation differs form JSON API - * definition of sorting - * in order to fit standard query parameter serializing strategy and maximize effective processing of data. - *

- * Sort params can be send with following format:
- * sort[ResourceType][property]([property])* = "asc|desc" - *

- * Examples of accepted sorting of resources: - *

    - *
  • {@code GET /tasks/?sort[tasks][name]=asc}
  • - *
  • {@code GET /project/?sort[projects][shortName]=desc&sort[users][name][firstName]=asc}
  • - *
- * - * @return {@link TypedParams} Map of sorting params passed to request grouped by type of resource - */ public TypedParams getSorting() { return sorting; } - - void setSorting(Map> sorting) { - Map> temporarySortingMap = new LinkedHashMap<>(); - - for (Map.Entry> entry : sorting.entrySet()) { - - List 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 resourceParams = temporarySortingMap.get(resourceType); - resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue() - .iterator() - .next())); - } else { - Map resourceParams = new HashMap<>(); - temporarySortingMap.put(resourceType, resourceParams); - resourceParams.put(propertyPath, RestrictedSortingValues.valueOf(entry.getValue() - .iterator() - .next())); - } - } - - Map decodedSortingMap = new LinkedHashMap<>(); - - for (Map.Entry> resourceTypesMap : temporarySortingMap.entrySet - ()) { - Map sortingMap = Collections.unmodifiableMap(resourceTypesMap.getValue()); - decodedSortingMap.put(resourceTypesMap.getKey(), new SortingParams(sortingMap)); - } - - - this.sorting = new TypedParams<>(Collections.unmodifiableMap(decodedSortingMap)); - + void setSorting(TypedParams sorting) { + this.sorting = sorting; } - /** - * Important: Grouping itself is not specified by JSON API itself, but the - * keyword and format it reserved for today and future use in Katharsis. - *

- * Group params can be send with following format:
- * group[ResourceType] = "property(.property)*" - *

- * Examples of accepted grouping of resources: - *

    - *
  • {@code GET /tasks/?group[tasks]=name}
  • - *
  • {@code GET /project/?group[users]=name.firstName&include[projects]=team}
  • - *
- * - * @return {@link Map} Map of grouping params passed to request grouped by type of resource - */ public TypedParams getGrouping() { return grouping; } - - void setGrouping(Map> grouping) { - Map> temporaryGroupingMap = new LinkedHashMap<>(); - - for (Map.Entry> entry : grouping.entrySet()) { - - List 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 resourceParams = temporaryGroupingMap.get(resourceType); - resourceParams.addAll(entry.getValue()); - temporaryGroupingMap.put(resourceType, resourceParams); - } else { - Set resourceParams = new LinkedHashSet<>(); - resourceParams.addAll(entry.getValue()); - temporaryGroupingMap.put(resourceType, resourceParams); - } - } - - Map decodedGroupingMap = new LinkedHashMap<>(); - - for (Map.Entry> resourceTypesMap : temporaryGroupingMap.entrySet()) { - Set groupingSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); - decodedGroupingMap.put(resourceTypesMap.getKey(), new GroupingParams(groupingSet)); - } - - this.grouping = new TypedParams<>(Collections.unmodifiableMap(decodedGroupingMap)); - + void setGrouping(TypedParams grouping) { + this.grouping = grouping; } - /** - * Important! Katharsis implementation sets on strategy of pagination whereas JSON API - * definition of pagination - * is agnostic about pagination strategies. - *

- * Pagination params can be send with following format:
- * page[offset|limit] = "value", where value is an integer - *

- * Examples of accepted grouping of resources: - *

    - *
  • {@code GET /projects/?page[offset]=0&page[limit]=10}
  • - *
- * - * @return {@link Map} Map of pagination keys passed to request - */ public Map getPagination() { return pagination; } - - void setPagination(Map> pagination) { - Map decodedPagination = new LinkedHashMap<>(); - - for (Map.Entry> entry : pagination.entrySet()) { - List 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); + void setPagination(Map pagination) { + this.pagination = pagination; } - /** - * Important! Katharsis implementation differs form JSON API - * definition of sparse field set - * in order to fit standard query parameter serializing strategy and maximize effective processing of data. - *

- * Sparse field set params can be send with following format:
- * fields[ResourceType] = "property(.property)*"
- *

- * Examples of accepted sparse field sets of resources: - *

    - *
  • {@code GET /tasks/?fields[tasks]=name}
  • - *
  • {@code GET /tasks/?fields[tasks][]=name&fields[tasks][]=dueDate}
  • - *
  • {@code GET /tasks/?fields[users]=name.surname&include[tasks]=author}
  • - *
- * - * @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource - */ public TypedParams getIncludedFields() { return includedFields; } - - void setIncludedFields(Map> sparse) { - Map> temporarySparseMap = new LinkedHashMap<>(); - - for (Map.Entry> entry : sparse.entrySet()) { - List 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 resourceParams = temporarySparseMap.get(resourceType); - resourceParams.addAll(entry.getValue()); - temporarySparseMap.put(resourceType, resourceParams); - } else { - Set resourceParams = new LinkedHashSet<>(); - resourceParams.addAll(entry.getValue()); - temporarySparseMap.put(resourceType, resourceParams); - } - } - - Map decodedSparseMap = new LinkedHashMap<>(); - - for (Map.Entry> resourceTypesMap : temporarySparseMap.entrySet()) { - Set sparseSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); - decodedSparseMap.put(resourceTypesMap.getKey(), new IncludedFieldsParams(sparseSet)); - } - - this.includedFields = new TypedParams<>(Collections.unmodifiableMap(decodedSparseMap)); + void setIncludedFields(TypedParams includedFields) { + this.includedFields = includedFields; } - /** - * Important! Katharsis implementation differs form JSON API - * definition of includes - * in order to fit standard query parameter serializing strategy and maximize effective processing of data. - *

- * Included field set params can be send with following format:
- * include[ResourceType] = "property(.property)*"
- *

- * Examples of accepted sparse field sets of resources: - *

    - *
  • {@code GET /tasks/?include[tasks]=author}
  • - *
  • {@code GET /tasks/?include[tasks][]=author&include[tasks][]=comments}
  • - *
  • {@code GET /projects/?include[projects]=task&include[tasks]=comments}
  • - *
- * - * @return {@link TypedParams} Map of sparse field set params passed to a request grouped by type of resource - */ public TypedParams getIncludedRelations() { return includedRelations; } - - void setIncludedRelations(Map> inclusions) { - Map> temporaryInclusionsMap = new LinkedHashMap<>(); - - for (Map.Entry> entry : inclusions.entrySet()) { - List 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 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 decodedInclusions = new LinkedHashMap<>(); - - for (Map.Entry> resourceTypesMap : temporaryInclusionsMap.entrySet()) { - Set inclusionSet = Collections.unmodifiableSet(resourceTypesMap.getValue()); - decodedInclusions.put(resourceTypesMap.getKey(), new IncludedRelationsParams(inclusionSet)); - } - - this.includedRelations = new TypedParams<>(Collections.unmodifiableMap(decodedInclusions)); - } - - private static List buildPropertyListFromEntry(Map.Entry> entry, String prefix) { - String entryKey = entry.getKey() - .substring(prefix.length()); - - String pattern = "[^\\]\\[]+(? matchList = new LinkedList<>(); - - while (matcher.find()) { - matchList.add(matcher.group()); - } - - - if (matchList.isEmpty()) { - throw new ParametersDeserializationException("Malformed filter parameter: " + entryKey); - } - - return matchList; + void setIncludedRelations(TypedParams includedRelations) { + this.includedRelations = includedRelations; } @Override diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsBuilder.java b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsBuilder.java index fe33f7c7..23546d34 100644 --- a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsBuilder.java +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsBuilder.java @@ -2,6 +2,8 @@ import io.katharsis.errorhandling.exception.KatharsisException; import io.katharsis.jackson.exception.ParametersDeserializationException; +import io.katharsis.queryParams.context.SimpleQueryParamsParserContext; +import io.katharsis.queryParams.context.QueryParamsParserContext; import java.util.Map; import java.util.Set; @@ -26,27 +28,33 @@ public class QueryParamsBuilder { public QueryParamsBuilder(final QueryParamsParser queryParamsParser) { this.queryParamsParser = queryParamsParser; } + /** - * Decodes passed query parameters + * Decodes passed query parameters using the given raw map. Mainly intended to be used for testing purposes. + * For most cases, use {@link #buildQueryParams(QueryParamsParserContext context) instead.} * * @param queryParams Map of provided query params * @return QueryParams containing filtered query params grouped by JSON:API standard * @throws ParametersDeserializationException thrown when unsupported input format is detected */ public QueryParams buildQueryParams(Map> queryParams) { - QueryParams deserializedQueryParams = new QueryParams(); + return buildQueryParams(new SimpleQueryParamsParserContext(queryParams)); + } + + /** + * Parses the query parameters of the current request using this builder's QueryParamsParser and the + * given context. + * @param context - Contains raw information about the query parameters of the current request + * @return - QueryParams object which contains the parsed query parameters of the current request + * @throws ParametersDeserializationException thrown when unsupported input format is detected + */ + public QueryParams buildQueryParams(QueryParamsParserContext context) { try { - deserializedQueryParams.setFilters(this.queryParamsParser.parseFiltersParameters(queryParams)); - deserializedQueryParams.setSorting(this.queryParamsParser.parseSortingParameters(queryParams)); - deserializedQueryParams.setGrouping(this.queryParamsParser.parseGroupingParameters(queryParams)); - deserializedQueryParams.setPagination(this.queryParamsParser.parsePaginationParameters(queryParams)); - deserializedQueryParams.setIncludedFields(this.queryParamsParser.parseIncludedFieldsParameters(queryParams)); - deserializedQueryParams.setIncludedRelations(this.queryParamsParser.parseIncludedRelationsParameters(queryParams)); + return queryParamsParser.parse(context); } catch (KatharsisException e) { throw e; } catch (RuntimeException e) { throw new ParametersDeserializationException(e.getMessage()); } - return deserializedQueryParams; } } diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsParser.java b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsParser.java index 1696bad3..026bf79f 100644 --- a/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsParser.java +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/QueryParamsParser.java @@ -17,16 +17,14 @@ package io.katharsis.queryParams; -import java.util.Map; -import java.util.Set; +import io.katharsis.queryParams.context.QueryParamsParserContext; +/** + * QueryParamsParser implementations will create QueryParams objects using the given + * QueryParamsParserContext. + * + * Known implementations: {@link DefaultQueryParamsParser} {@link JsonApiQueryParamsParser} + */ public interface QueryParamsParser { - - Map> parseFiltersParameters(Map> queryParams); - Map> parseSortingParameters(Map> queryParams); - Map> parseGroupingParameters(Map> queryParams); - Map> parseIncludedFieldsParameters(Map> queryParams); - Map> parseIncludedRelationsParameters(Map> queryParams); - Map> parsePaginationParameters(Map> queryParams); - + QueryParams parse(QueryParamsParserContext context); } diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/context/AbstractQueryParamsParserContext.java b/katharsis-core/src/main/java/io/katharsis/queryParams/context/AbstractQueryParamsParserContext.java new file mode 100644 index 00000000..6a0b0e0a --- /dev/null +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/context/AbstractQueryParamsParserContext.java @@ -0,0 +1,18 @@ +package io.katharsis.queryParams.context; + + +import io.katharsis.request.path.JsonPath; +import io.katharsis.resource.information.ResourceInformation; +import io.katharsis.resource.registry.ResourceRegistry; + +public abstract class AbstractQueryParamsParserContext implements QueryParamsParserContext { + + private final ResourceInformation resourceInformation; + + protected AbstractQueryParamsParserContext(ResourceRegistry resourceRegistry, JsonPath path) { + resourceInformation = resourceRegistry.getEntry(path.getResourceName()).getResourceInformation(); + } + + @Override + public ResourceInformation getRequestedResourceInformation() { return resourceInformation; } +} diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/context/QueryParamsParserContext.java b/katharsis-core/src/main/java/io/katharsis/queryParams/context/QueryParamsParserContext.java new file mode 100644 index 00000000..9863498c --- /dev/null +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/context/QueryParamsParserContext.java @@ -0,0 +1,27 @@ +package io.katharsis.queryParams.context; + +import io.katharsis.resource.information.ResourceInformation; + +import java.util.Set; + +/** + * Supplies information about the query parameters of the incoming request. This information is then + * used by QueryParamsParsers to create QueryParams objects. + */ +public interface QueryParamsParserContext { + + /** + * Returns the set of parameter values that match the given query parameter name of the current request. + */ + Set getParameterValue(String parameterName); + + /** + * Returns the set of query parameter names associated to the current request. + */ + Iterable getParameterNames(); + + /** + * Returns ResourceInformation for the primary resource of the current request. + */ + ResourceInformation getRequestedResourceInformation(); +} diff --git a/katharsis-core/src/main/java/io/katharsis/queryParams/context/SimpleQueryParamsParserContext.java b/katharsis-core/src/main/java/io/katharsis/queryParams/context/SimpleQueryParamsParserContext.java new file mode 100644 index 00000000..f3e3ce39 --- /dev/null +++ b/katharsis-core/src/main/java/io/katharsis/queryParams/context/SimpleQueryParamsParserContext.java @@ -0,0 +1,41 @@ +package io.katharsis.queryParams.context; + + +import io.katharsis.resource.information.ResourceInformation; + +import java.util.Map; +import java.util.Set; + +/** + * A QueryParamsParserContext implementation mainly used for testing purposes. + * This implementation is a simple wrapper over a map of query parameters and their values. + */ +public class SimpleQueryParamsParserContext implements QueryParamsParserContext { + + private final Map> paramMap; + private final ResourceInformation resourceInformation; + + public SimpleQueryParamsParserContext(Map> paramMap) { + this(paramMap, null); + } + + public SimpleQueryParamsParserContext(Map> paramMap, ResourceInformation resourceInformation) { + this.paramMap = paramMap; + this.resourceInformation = resourceInformation; + } + + @Override + public Set getParameterValue(String parameterName) { + return paramMap.get(parameterName); + } + + @Override + public Iterable getParameterNames() { + return paramMap.keySet(); + } + + @Override + public ResourceInformation getRequestedResourceInformation() { + return resourceInformation; + } +} diff --git a/katharsis-core/src/test/java/io/katharsis/module/TestResource.java b/katharsis-core/src/test/java/io/katharsis/module/TestResource.java index 5c2137eb..77a9f6d7 100644 --- a/katharsis-core/src/test/java/io/katharsis/module/TestResource.java +++ b/katharsis-core/src/test/java/io/katharsis/module/TestResource.java @@ -1,6 +1,6 @@ package io.katharsis.module; -class TestResource { +public class TestResource { private int id; diff --git a/katharsis-core/src/test/java/io/katharsis/module/TestResourceInformationBuilder.java b/katharsis-core/src/test/java/io/katharsis/module/TestResourceInformationBuilder.java index 5e5e18c8..3eecfafa 100644 --- a/katharsis-core/src/test/java/io/katharsis/module/TestResourceInformationBuilder.java +++ b/katharsis-core/src/test/java/io/katharsis/module/TestResourceInformationBuilder.java @@ -8,7 +8,7 @@ import io.katharsis.resource.information.ResourceInformation; import io.katharsis.resource.information.ResourceInformationBuilder; -class TestResourceInformationBuilder implements ResourceInformationBuilder { +public class TestResourceInformationBuilder implements ResourceInformationBuilder { @Override public boolean accept(Class resourceClass) { @@ -19,7 +19,7 @@ public boolean accept(Class resourceClass) { public ResourceInformation build(Class resourceClass) { ResourceField idField = new ResourceField("testId", "id", Integer.class, null); ResourceAttributesBridge attributeFields = null; - Set relationshipFields = new HashSet(); + Set relationshipFields = new HashSet<>(); ResourceInformation info = new ResourceInformation(resourceClass, resourceClass.getSimpleName(), idField, attributeFields, relationshipFields); return info; diff --git a/katharsis-core/src/test/java/io/katharsis/queryParams/DefaultQueryParamsParserTest.java b/katharsis-core/src/test/java/io/katharsis/queryParams/DefaultQueryParamsParserTest.java index a26aa465..d4d3abb5 100644 --- a/katharsis-core/src/test/java/io/katharsis/queryParams/DefaultQueryParamsParserTest.java +++ b/katharsis-core/src/test/java/io/katharsis/queryParams/DefaultQueryParamsParserTest.java @@ -1,9 +1,16 @@ package io.katharsis.queryParams; +import io.katharsis.module.TestResource; +import io.katharsis.module.TestResourceInformationBuilder; +import io.katharsis.queryParams.context.SimpleQueryParamsParserContext; +import io.katharsis.queryParams.include.Inclusion; import org.junit.Before; import org.junit.Test; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -24,12 +31,12 @@ public void onGivenFiltersParserShouldReturnOnlyRequestParamsWithFilters() { queryParams.put("random[users][name]", Collections.singleton("John")); // WHEN - Map> result = parser.parseFiltersParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(1); - assertThat(result.entrySet().iterator().next().getKey().startsWith("filter")); - assertThat(result.entrySet().iterator().next().getValue().equals(Collections.singleton("John"))); + assertThat(result.getFilters().getParams().size()).isEqualTo(1); + assertThat(result.getFilters().getParams().get("users").getParams().size()).isEqualTo(1); + assertThat(result.getFilters().getParams().get("users").getParams().get("name")).isEqualTo(Collections.singleton("John")); } @Test @@ -39,12 +46,12 @@ public void onGivenSortingParserShouldReturnOnlyRequestParamsWithSorting() { queryParams.put("random[users][name]", Collections.singleton("desc")); // WHEN - Map> result = parser.parseSortingParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(1); - assertThat(result.entrySet().iterator().next().getKey().startsWith("sort")); - assertThat(result.entrySet().iterator().next().getValue().equals(Collections.singleton("asc"))); + assertThat(result.getSorting().getParams().size()).isEqualTo(1); + assertThat(result.getSorting().getParams().get("users").getParams().size()).isEqualTo(1); + assertThat(result.getSorting().getParams().get("users").getParams().get("name")).isEqualTo(RestrictedSortingValues.asc); } @Test @@ -54,12 +61,11 @@ public void onGivenGroupingParserShouldReturnOnlyRequestParamsWithGrouping() { queryParams.put("random[users]", Collections.singleton("surname")); // WHEN - Map> result = parser.parseGroupingParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(1); - assertThat(result.entrySet().iterator().next().getKey().startsWith("group")); - assertThat(result.entrySet().iterator().next().getValue().equals(Collections.singleton("name"))); + assertThat(result.getGrouping().getParams().size()).isEqualTo(1); + assertThat(result.getGrouping().getParams()).containsOnlyKeys("users"); } @Test @@ -71,15 +77,14 @@ public void onGivenPaginationParserShouldReturnOnlyRequestParamsWithPagination() queryParams.put("random[limit]", Collections.singleton("20")); // WHEN - Map> result = parser.parsePaginationParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(2); - assertThat(result.get("page[offset]").equals(Collections.singleton("1"))); - assertThat(result.get("page[limit]").equals(Collections.singleton("10"))); + assertThat(result.getPagination().size()).isEqualTo(2); + assertThat(result.getPagination().get(RestrictedPaginationKeys.offset)).isEqualTo(1); + assertThat(result.getPagination().get(RestrictedPaginationKeys.limit)).isEqualTo(10); } - //////// @Test public void onGivenIncludedFieldsParserShouldReturnOnlyRequestParamsWithIncludedFields() { // GIVEN @@ -87,12 +92,11 @@ public void onGivenIncludedFieldsParserShouldReturnOnlyRequestParamsWithIncluded queryParams.put("random[users]", Collections.singleton("surname")); // WHEN - Map> result = parser.parseIncludedFieldsParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(1); - assertThat(result.entrySet().iterator().next().getKey().startsWith("fields")); - assertThat(result.entrySet().iterator().next().getValue().equals(Collections.singleton("name"))); + assertThat(result.getIncludedFields().getParams().size()).isEqualTo(1); + assertThat(result.getIncludedFields().getParams().get("users").getParams()).containsOnly("name"); } @Test @@ -102,12 +106,15 @@ public void onGivenIncludedRelationsParserShouldReturnOnlyRequestParamsWithInclu queryParams.put("random[user]", Collections.singleton("surname")); // WHEN - Map> result = parser.parseIncludedRelationsParameters(queryParams); + QueryParams result = parseQueryParams(); // THEN - assertThat(result.entrySet().size()).isEqualTo(1); - assertThat(result.entrySet().iterator().next().getKey().startsWith("include")); - assertThat(result.entrySet().iterator().next().getValue().equals(Collections.singleton("name"))); + assertThat(result.getIncludedRelations().getParams().size()).isEqualTo(1); + assertThat(result.getIncludedRelations().getParams().get("user").getParams()).containsOnly(new Inclusion("name")); } + private QueryParams parseQueryParams() { + return parser.parse(new SimpleQueryParamsParserContext(queryParams, + new TestResourceInformationBuilder().build(TestResource.class))); + } } diff --git a/katharsis-core/src/test/java/io/katharsis/queryParams/JsonApiQueryParamsParserTest.java b/katharsis-core/src/test/java/io/katharsis/queryParams/JsonApiQueryParamsParserTest.java new file mode 100644 index 00000000..d03b3485 --- /dev/null +++ b/katharsis-core/src/test/java/io/katharsis/queryParams/JsonApiQueryParamsParserTest.java @@ -0,0 +1,77 @@ +package io.katharsis.queryParams; + + +import io.katharsis.module.TestResource; +import io.katharsis.module.TestResourceInformationBuilder; +import io.katharsis.queryParams.context.SimpleQueryParamsParserContext; +import io.katharsis.queryParams.include.Inclusion; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonApiQueryParamsParserTest { + + private Map> queryParams; + private QueryParamsParser parser = new JsonApiQueryParamsParser(); + + @Before + public void prepare() { + queryParams = new HashMap<>(); + } + + @Test + public void onGivenSortingParserShouldReturnOnlyRequestParamsWithSorting() { + // GIVEN + queryParams.put("sort", Collections.singleton("name,-id")); + queryParams.put("random", Collections.singleton("name,-id")); + + // WHEN + QueryParams result = parseQueryParams(); + + // THEN + assertThat(result.getSorting().getParams()).containsOnlyKeys(TestResource.class.getSimpleName()); + assertThat(result.getSorting().getParams().get(TestResource.class.getSimpleName()).getParams().size()).isEqualTo(2); + assertThat(result.getSorting().getParams().get(TestResource.class.getSimpleName()).getParams().get("name")).isEqualTo(RestrictedSortingValues.asc); + assertThat(result.getSorting().getParams().get(TestResource.class.getSimpleName()).getParams().get("id")).isEqualTo(RestrictedSortingValues.desc); + } + + @Test + public void onGivenIncludedFieldsParserShouldReturnOnlyRequestParamsWithIncludedFields() { + // GIVEN + queryParams.put("fields[users]", Collections.singleton("name,id")); + queryParams.put("random[users]", Collections.singleton("surname")); + + // WHEN + QueryParams result = parseQueryParams(); + + // THEN + assertThat(result.getIncludedFields().getParams().size()).isEqualTo(1); + assertThat(result.getIncludedFields().getParams().get("users").getParams()).containsOnly("name", "id"); + } + + @Test + public void onGivenIncludedRelationsParserShouldReturnOnlyRequestParamsWithIncludedRelations() { + // GIVEN + queryParams.put("include", Collections.singleton("author,comments.author")); + queryParams.put("random", Collections.singleton("author")); + + // WHEN + QueryParams result = parseQueryParams(); + + // THEN + assertThat(result.getIncludedRelations().getParams().size()).isEqualTo(1); + assertThat(result.getIncludedRelations().getParams().get(TestResource.class.getSimpleName()). + getParams()).containsOnly(new Inclusion("author"), new Inclusion("comments.author")); + } + + private QueryParams parseQueryParams() { + return parser.parse(new SimpleQueryParamsParserContext(queryParams, + new TestResourceInformationBuilder().build(TestResource.class))); + } +} diff --git a/katharsis-core/src/test/java/io/katharsis/queryParams/QueryParamsBuilderTest.java b/katharsis-core/src/test/java/io/katharsis/queryParams/QueryParamsBuilderTest.java index 2612a61c..93561e47 100644 --- a/katharsis-core/src/test/java/io/katharsis/queryParams/QueryParamsBuilderTest.java +++ b/katharsis-core/src/test/java/io/katharsis/queryParams/QueryParamsBuilderTest.java @@ -170,4 +170,10 @@ public void onGivenIncludedRelationsBuilderShouldReturnRequestParamsWithIncluded .get("special-users") .getParams()).containsExactly(new Inclusion("friends"), new Inclusion("foes")); } + + @Test(expected = ParametersDeserializationException.class) + public void onGivenInvalidQueryParamsBuilderShouldThrowParametersDeserializationException() { + queryParams.put("include[missing_bracket", new LinkedHashSet<>(Arrays.asList("bad","request"))); + sut.buildQueryParams(queryParams); + } } diff --git a/katharsis-rs/src/main/java/io/katharsis/rs/JaxRsQueryParamsParserContext.java b/katharsis-rs/src/main/java/io/katharsis/rs/JaxRsQueryParamsParserContext.java new file mode 100644 index 00000000..45551875 --- /dev/null +++ b/katharsis-rs/src/main/java/io/katharsis/rs/JaxRsQueryParamsParserContext.java @@ -0,0 +1,41 @@ +package io.katharsis.rs; + +import io.katharsis.queryParams.context.AbstractQueryParamsParserContext; +import io.katharsis.request.path.JsonPath; +import io.katharsis.resource.registry.ResourceRegistry; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; +import java.util.*; + + +public class JaxRsQueryParamsParserContext extends AbstractQueryParamsParserContext { + + private final Map> queryParameters = new HashMap<>(); + + JaxRsQueryParamsParserContext(UriInfo uriInfo, ResourceRegistry resourceRegistry, JsonPath path) { + super(resourceRegistry, path); + initParameterMap(uriInfo); + } + + @Override + public Set getParameterValue(String parameterName) { + if (queryParameters.containsKey(parameterName)) { + return queryParameters.get(parameterName); + } + return Collections.emptySet(); + } + + @Override + public Iterable getParameterNames() { + return queryParameters.keySet(); + } + + private void initParameterMap(UriInfo uriInfo) { + MultivaluedMap queryParametersMultiMap = uriInfo.getQueryParameters(); + + for (Map.Entry> queryEntry : queryParametersMultiMap.entrySet()) { + queryParameters.put(queryEntry.getKey(), new LinkedHashSet<>(queryEntry.getValue())); + } + } +} diff --git a/katharsis-rs/src/main/java/io/katharsis/rs/KatharsisFilter.java b/katharsis-rs/src/main/java/io/katharsis/rs/KatharsisFilter.java index 73792301..11bdd980 100644 --- a/katharsis-rs/src/main/java/io/katharsis/rs/KatharsisFilter.java +++ b/katharsis-rs/src/main/java/io/katharsis/rs/KatharsisFilter.java @@ -116,7 +116,7 @@ private void dispatchRequest(ContainerRequestContext requestContext) throws Exce ResourceRegistry newRegistry = new ResourceRegistry(resourceRegistry.getResources(), new UriInfoServiceUrlProvider(uriInfo)); JsonPath jsonPath = new PathBuilder(newRegistry).buildPath(path); - QueryParams requestParams = createQueryParams(uriInfo); + QueryParams requestParams = createQueryParams(uriInfo, jsonPath); String method = requestContext.getMethod(); RequestBody requestBody = inputStreamToBody(requestContext.getEntityStream()); @@ -161,15 +161,8 @@ private void abortWithResponse(ContainerRequestContext requestContext, BaseRespo requestContext.abortWith(response); } - private QueryParams createQueryParams(UriInfo uriInfo) { - MultivaluedMap queryParametersMultiMap = uriInfo.getQueryParameters(); - Map> queryParameters = new HashMap<>(); - - for (Map.Entry> queryEntry : queryParametersMultiMap.entrySet()) { - queryParameters.put(queryEntry.getKey(), new LinkedHashSet<>(queryEntry.getValue())); - } - - return queryParamsBuilder.buildQueryParams(queryParameters); + private QueryParams createQueryParams(UriInfo uriInfo, JsonPath path) { + return queryParamsBuilder.buildQueryParams(new JaxRsQueryParamsParserContext(uriInfo, resourceRegistry, path)); } public RequestBody inputStreamToBody(InputStream is) throws IOException { diff --git a/katharsis-servlet/src/main/java/io/katharsis/invoker/KatharsisInvoker.java b/katharsis-servlet/src/main/java/io/katharsis/invoker/KatharsisInvoker.java index 3a63a66e..c452a596 100644 --- a/katharsis-servlet/src/main/java/io/katharsis/invoker/KatharsisInvoker.java +++ b/katharsis-servlet/src/main/java/io/katharsis/invoker/KatharsisInvoker.java @@ -31,15 +31,13 @@ import io.katharsis.request.path.PathBuilder; import io.katharsis.resource.registry.ResourceRegistry; import io.katharsis.response.BaseResponseContext; -import io.katharsis.servlet.util.QueryStringUtils; +import io.katharsis.servlet.ServletQueryParamsParserContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletResponse; import java.io.*; -import java.util.Map; import java.util.Scanner; -import java.util.Set; /** * Katharsis dispatcher invoker. @@ -85,7 +83,7 @@ private void dispatchRequest(KatharsisInvokerContext invokerContext) throws Exce try { JsonPath jsonPath = new PathBuilder(resourceRegistry).buildPath(invokerContext.getRequestPath()); - QueryParams queryParams = createQueryParams(invokerContext); + QueryParams queryParams = createQueryParams(invokerContext, jsonPath); in = invokerContext.getRequestEntityStream(); RequestBody requestBody = inputStreamToBody(in); @@ -150,10 +148,9 @@ private boolean isAcceptableMediaType(KatharsisInvokerContext invokerContext) { return false; } - private QueryParams createQueryParams(KatharsisInvokerContext invokerContext) { - Map> queryParameters = - QueryStringUtils.parseQueryStringAsSingleValueMap(invokerContext); - return this.queryParamsBuilder.buildQueryParams(queryParameters); + private QueryParams createQueryParams(KatharsisInvokerContext invokerContext, JsonPath path) { + return queryParamsBuilder.buildQueryParams(new ServletQueryParamsParserContext(invokerContext, + resourceRegistry, path)); } private RequestBody inputStreamToBody(InputStream is) throws IOException { diff --git a/katharsis-servlet/src/main/java/io/katharsis/servlet/ServletQueryParamsParserContext.java b/katharsis-servlet/src/main/java/io/katharsis/servlet/ServletQueryParamsParserContext.java new file mode 100644 index 00000000..f932c89b --- /dev/null +++ b/katharsis-servlet/src/main/java/io/katharsis/servlet/ServletQueryParamsParserContext.java @@ -0,0 +1,38 @@ +package io.katharsis.servlet; + +import io.katharsis.invoker.KatharsisInvokerContext; +import io.katharsis.queryParams.context.AbstractQueryParamsParserContext; +import io.katharsis.queryParams.context.QueryParamsParserContext; +import io.katharsis.request.path.JsonPath; +import io.katharsis.resource.information.ResourceInformation; +import io.katharsis.resource.registry.ResourceRegistry; +import io.katharsis.servlet.util.QueryStringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class ServletQueryParamsParserContext extends AbstractQueryParamsParserContext { + + private final Map> queryParameters; + + public ServletQueryParamsParserContext(KatharsisInvokerContext invokerContext, ResourceRegistry resourceRegistry, + JsonPath path) { + super(resourceRegistry, path); + queryParameters = QueryStringUtils.parseQueryStringAsSingleValueMap(invokerContext); + } + + @Override + public Set getParameterValue(String parameterName) { + if (queryParameters.containsKey(parameterName)) { + return queryParameters.get(parameterName); + } + return Collections.emptySet(); + } + + @Override + public Iterable getParameterNames() { + return queryParameters.keySet(); + } +} diff --git a/katharsis-spring/src/main/java/io/katharsis/spring/KatharsisFilterV2.java b/katharsis-spring/src/main/java/io/katharsis/spring/KatharsisFilterV2.java index 5bf1a882..0c82c7f3 100644 --- a/katharsis-spring/src/main/java/io/katharsis/spring/KatharsisFilterV2.java +++ b/katharsis-spring/src/main/java/io/katharsis/spring/KatharsisFilterV2.java @@ -113,7 +113,7 @@ private boolean dispatchRequest(HttpServletRequest request, HttpServletResponse try { JsonPath jsonPath = new PathBuilder(resourceRegistry).buildPath(getRequestPath(request)); - QueryParams queryParams = createQueryParams(request); + QueryParams queryParams = createQueryParams(request, jsonPath); in = request.getInputStream(); RequestBody requestBody = inputStreamToBody(in); @@ -210,16 +210,11 @@ private boolean isAcceptableMediaType(HttpServletRequest servletRequest) { * body parameters, but we don't expect to receive such body. * * @param request request body + * @path * @return query parameters */ - private QueryParams createQueryParams(HttpServletRequest request) { - Map params = request.getParameterMap(); - - Map> queryParameters = new HashMap<>(params.size()); - for (Map.Entry entry : params.entrySet()) { - queryParameters.put(entry.getKey(), new HashSet<>(Arrays.asList(entry.getValue()))); - } - return queryParamsBuilder.buildQueryParams(queryParameters); + private QueryParams createQueryParams(HttpServletRequest request, JsonPath path) { + return queryParamsBuilder.buildQueryParams(new SpringQueryParamsParserContext(request, resourceRegistry, path)); } private RequestBody inputStreamToBody(InputStream is) { diff --git a/katharsis-spring/src/main/java/io/katharsis/spring/SpringQueryParamsParserContext.java b/katharsis-spring/src/main/java/io/katharsis/spring/SpringQueryParamsParserContext.java new file mode 100644 index 00000000..3626defd --- /dev/null +++ b/katharsis-spring/src/main/java/io/katharsis/spring/SpringQueryParamsParserContext.java @@ -0,0 +1,40 @@ +package io.katharsis.spring; + +import io.katharsis.queryParams.context.AbstractQueryParamsParserContext; +import io.katharsis.queryParams.context.QueryParamsParserContext; +import io.katharsis.request.path.JsonPath; +import io.katharsis.resource.information.ResourceInformation; +import io.katharsis.resource.registry.ResourceRegistry; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +public class SpringQueryParamsParserContext extends AbstractQueryParamsParserContext { + + private final Map> queryParameters = new HashMap<>(); + + SpringQueryParamsParserContext(HttpServletRequest request, ResourceRegistry resourceRegistry, JsonPath path) { + super(resourceRegistry, path); + initParameterMap(request); + } + + @Override + public Set getParameterValue(String parameterName) { + if (queryParameters.containsKey(parameterName)) { + return queryParameters.get(parameterName); + } + return Collections.emptySet(); + } + + @Override + public Iterable getParameterNames() { + return queryParameters.keySet(); + } + + private void initParameterMap(HttpServletRequest request) { + Map params = request.getParameterMap(); + for (Map.Entry entry : params.entrySet()) { + queryParameters.put(entry.getKey(), new HashSet<>(Arrays.asList(entry.getValue()))); + } + } +}