Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): complete advance search for components #2843

Merged
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
Expand Up @@ -317,8 +317,14 @@ private static String sanitizeQueryInput(String input) {
return "[" + dateToNouveauDouble(dates[0]) + RANGE_TO + dateToNouveauDouble(dates[1]) + "]";
}

/**
* Parse dates from String in (yyyy-MM-dd) format to Nouveau format (yyyyMMdd) which is used as a double in queries.
* @param date Date to convert
* @return Parsed date for Nouveau
* @throws ParseException If input date cannot be parsed
* @see #dateToNouveauFormat(Date)
*/
public static @NotNull String dateToNouveauDouble(String date) throws ParseException {
SimpleDateFormat outputFormatter = new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat inputFormatterDate = new SimpleDateFormat("yyyy-MM-dd");
Date parsedDate;
try {
Expand All @@ -328,6 +334,17 @@ private static String sanitizeQueryInput(String input) {
} catch (Exception e) {
throw new ParseException("Date format not recognized", 0);
}
return outputFormatter.format(parsedDate.getTime());
return dateToNouveauFormat(parsedDate);
}

/**
* Convert a java.util.Date object to Nouveau format (yyyyMMdd) which is used as a double in queries.
* @param date Date to convert
* @return Parsed date for Nouveau
* @see #dateToNouveauDouble(String)
*/
public static @NotNull String dateToNouveauFormat(Date date) {
SimpleDateFormat outputFormatter = new SimpleDateFormat("yyyyMMdd");
return outputFormatter.format(date.getTime());
}
}
20 changes: 20 additions & 0 deletions rest/resource-server/src/docs/asciidoc/components.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ include::{snippets}/should_document_get_components_by_name/http-response.adoc[]
include::{snippets}/should_document_get_components_by_name/links.adoc[]


[[resources-components-list-by-type-and-createdon]]
==== Filtering with more fields

A `GET` request to fetch filtered list of service components.

Note : send query parameter's value in encoded format. (Reference: `https://datatracker.ietf.org/doc/html/rfc3986`)

===== Response structure
include::{snippets}/should_document_get_components_by_type_and_created_on/response-fields.adoc[]

===== Example request
include::{snippets}/should_document_get_components_by_type_and_created_on/curl-request.adoc[]

===== Example response
include::{snippets}/should_document_get_components_by_type_and_created_on/http-response.adoc[]

===== Links
include::{snippets}/should_document_get_components_by_type_and_created_on/links.adoc[]


[[resources-components-list-by-type]]
==== Listing by type

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

package org.eclipse.sw360.rest.resourceserver.component;

import com.google.common.collect.Sets;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
Expand All @@ -37,6 +38,7 @@
import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentDTO;
import org.eclipse.sw360.datahandler.thrift.components.Component;
import org.eclipse.sw360.datahandler.thrift.components.ComponentDTO;
import org.eclipse.sw360.datahandler.thrift.components.ComponentType;
import org.eclipse.sw360.datahandler.thrift.components.Release;
import org.eclipse.sw360.datahandler.thrift.components.ReleaseLink;
import org.eclipse.sw360.datahandler.thrift.projects.Project;
Expand Down Expand Up @@ -71,7 +73,6 @@
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
Expand All @@ -87,13 +88,11 @@
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
Expand Down Expand Up @@ -141,13 +140,31 @@ public ResponseEntity<CollectionModel<EntityModel<Component>>> getComponents(
Pageable pageable,
@Parameter(description = "Name of the component to filter")
@RequestParam(value = "name", required = false) String name,
@Parameter(description = "Type of the component to filter")
@Parameter(description = "Categories of the component to filter, as a comma separated list.")
@RequestParam(value = "categories", required = false) String categories,
@Parameter(description = "Type of the component to filter",
schema = @Schema(implementation = ComponentType.class))
@RequestParam(value = "type", required = false) String componentType,
@Parameter(description = "Component languages to filter, as a comma separated list.")
@RequestParam(value = "languages", required = false) String languages,
@Parameter(description = "Software Platforms to filter, as a comma separated list.")
@RequestParam(value = "softwarePlatforms", required = false) String softwarePlatforms,
@Parameter(description = "Operating Systems to filter, as a comma separated list.")
@RequestParam(value = "operatingSystems", required = false) String operatingSystems,
@Parameter(description = "Vendors to filter, as a comma separated list.")
@RequestParam(value = "vendors", required = false) String vendors,
@Parameter(description = "Main Licenses to filter, as a comma separated list.")
@RequestParam(value = "mainLicenses", required = false) String mainLicenses,
@Parameter(description = "Created by user to filter (email).")
@RequestParam(value = "createdBy", required = false) String createdBy,
@Parameter(description = "Date component was created on (YYYY-MM-DD).",
schema = @Schema(type = "string", format = "date"))
@RequestParam(value = "createdOn", required = false) String createdOn,
@Parameter(description = "Properties which should be present for each component in the result")
@RequestParam(value = "fields", required = false) List<String> fields,
@Parameter(description = "Flag to get components with all details.")
@RequestParam(value = "allDetails", required = false) boolean allDetails,
@Parameter(description = "lucenesearch parameter to filter the components.")
@Parameter(description = "Use lucenesearch to filter the components.")
@RequestParam(value = "luceneSearch", required = false) boolean luceneSearch,
HttpServletRequest request
) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException {
Expand All @@ -158,12 +175,9 @@ public ResponseEntity<CollectionModel<EntityModel<Component>>> getComponents(
String queryString = request.getQueryString();
Map<String, String> params = restControllerHelper.parseQueryString(queryString);

Map<String, Set<String>> filterMap = new HashMap<>();
if (luceneSearch) {
if (CommonUtils.isNotNullEmptyOrWhitespace(componentType)) {
Set<String> values = CommonUtils.splitToSet(componentType);
filterMap.put(Component._Fields.COMPONENT_TYPE.getFieldName(), values);
}
Map<String, Set<String>> filterMap = getFilterMap(categories, componentType, languages, softwarePlatforms,
operatingSystems, vendors, mainLicenses, createdBy, createdOn);
if (CommonUtils.isNotNullEmptyOrWhitespace(name)) {
Set<String> values = CommonUtils.splitToSet(name);
values = values.stream().map(NouveauLuceneAwareDatabaseConnector::prepareWildcardQuery)
Expand All @@ -177,19 +191,24 @@ public ResponseEntity<CollectionModel<EntityModel<Component>>> getComponents(
} else {
allComponents.addAll(componentService.getComponentsForUser(sw360User));
}
Map<String, Set<String>> restrictions = getFilterMap(categories, componentType, languages,
softwarePlatforms, operatingSystems, vendors, mainLicenses, createdBy, createdOn);
if (!restrictions.isEmpty()) {
allComponents = new ArrayList<>(allComponents.stream()
.filter(filterComponentMap(restrictions)).toList());
}
}

PaginationResult<Component> paginationResult = restControllerHelper.createPaginationResult(request, pageable,
allComponents, SW360Constants.TYPE_COMPONENT);

CollectionModel resources = getFilteredComponentResources(componentType, fields, allDetails, luceneSearch,
sw360User, paginationResult);
CollectionModel resources = getFilteredComponentResources(fields, allDetails, sw360User, paginationResult);
return new ResponseEntity<>(resources, HttpStatus.OK);
}

private CollectionModel getFilteredComponentResources(String componentType, List<String> fields, boolean allDetails,
boolean luceneSearch, User sw360User, PaginationResult<Component> paginationResult)
throws URISyntaxException {
private CollectionModel getFilteredComponentResources(
List<String> fields, boolean allDetails, User sw360User, PaginationResult<Component> paginationResult
) throws URISyntaxException {
List<EntityModel<Component>> componentResources = new ArrayList<>();
Consumer<Component> consumer = c -> {
EntityModel<Component> embeddedComponentResource = null;
Expand All @@ -209,14 +228,7 @@ private CollectionModel getFilteredComponentResources(String componentType, List
componentResources.add(embeddedComponentResource);
};

if (luceneSearch) {
paginationResult.getResources().stream().forEach(consumer);
} else {
paginationResult.getResources().stream()
.filter(component -> componentType == null
|| (component.isSetComponentType() && componentType.equals(component.componentType.name())))
.forEach(consumer);
}
paginationResult.getResources().forEach(consumer);

CollectionModel resources;
if (componentResources.isEmpty()) {
Expand Down Expand Up @@ -1092,4 +1104,72 @@ public ResponseEntity<RequestStatus> splitComponents(

return new ResponseEntity<>(requestStatus, HttpStatus.OK);
}

/**
* Create a map of filters with the field name in the key and expected value in the value (as set).
* @return Filter map from the user's request.
*/
private @NonNull Map<String, Set<String>> getFilterMap(
String categories, String componentType, String languages, String softwarePlatforms,
String operatingSystems, String vendors, String mainLicenses, String createdBy, String createdOn
) {
Map<String, Set<String>> filterMap = new HashMap<>();
if (CommonUtils.isNotNullEmptyOrWhitespace(categories)) {
filterMap.put(Component._Fields.CATEGORIES.getFieldName(), CommonUtils.splitToSet(categories));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(componentType)) {
filterMap.put(Component._Fields.COMPONENT_TYPE.getFieldName(), CommonUtils.splitToSet(componentType));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(languages)) {
filterMap.put(Component._Fields.LANGUAGES.getFieldName(), CommonUtils.splitToSet(languages));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(softwarePlatforms)) {
filterMap.put(Component._Fields.SOFTWARE_PLATFORMS.getFieldName(), CommonUtils.splitToSet(softwarePlatforms));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(operatingSystems)) {
filterMap.put(Component._Fields.OPERATING_SYSTEMS.getFieldName(), CommonUtils.splitToSet(operatingSystems));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(vendors)) {
filterMap.put(Component._Fields.VENDOR_NAMES.getFieldName(), CommonUtils.splitToSet(vendors));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(mainLicenses)) {
filterMap.put(Component._Fields.MAIN_LICENSE_IDS.getFieldName(), CommonUtils.splitToSet(mainLicenses));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(createdBy)) {
filterMap.put(Component._Fields.CREATED_BY.getFieldName(), CommonUtils.splitToSet(createdBy));
}
if (CommonUtils.isNotNullEmptyOrWhitespace(createdOn)) {
filterMap.put(Component._Fields.CREATED_ON.getFieldName(), CommonUtils.splitToSet(createdOn));
}
return filterMap;
}

/**
* Create a filter predicate to remove all components which do not satisfy the restriction set.
* @param restrictions Restrictions set to filter components on
* @return Filter predicate for stream.
*/
private static @NonNull Predicate<Component> filterComponentMap(Map<String, Set<String>> restrictions) {
return component -> {
for (Map.Entry<String, Set<String>> restriction : restrictions.entrySet()) {
final Set<String> filterSet = restriction.getValue();
Component._Fields field = Component._Fields.findByName(restriction.getKey());
Object fieldValue = component.getFieldValue(field);
if (fieldValue == null) {
return false;
}
if (field == Component._Fields.COMPONENT_TYPE && !filterSet.contains(component.componentType.name())) {
return false;
} else if ((field == Component._Fields.CREATED_BY || field == Component._Fields.CREATED_ON)
&& !fieldValue.toString().equalsIgnoreCase(filterSet.iterator().next())) {
return false;
} else if (fieldValue instanceof Set) {
if (Sets.intersection(filterSet, (Set<String>) fieldValue).isEmpty()) {
return false;
}
}
}
return true;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,47 @@ public void should_document_get_components_by_name() throws Exception {
)));
}

@Test
public void should_document_get_components_by_type_and_created_on() throws Exception {
mockMvc.perform(get("/api/components")
.header("Authorization", TestHelper.generateAuthHeader(testUserId, testUserPassword))
.queryParam("componentType", angularComponent.getComponentType().toString())
.queryParam("createdOn", angularComponent.getCreatedOn())
.queryParam("categories", "javascript, sql")
.queryParam("luceneSearch", "false")
.queryParam("page", "0")
.queryParam("page_entries", "5")
.queryParam("sort", "name,desc")
.accept(MediaTypes.HAL_JSON))
.andExpect(status().isOk())
.andDo(this.documentationHandler.document(
queryParameters(
parameterWithName("componentType").description("Filter for type"),
parameterWithName("createdOn").description("Filter for component creation date"),
parameterWithName("categories").description("Filter for categories"),
parameterWithName("luceneSearch").description("Filter with exact match or lucene match."),
parameterWithName("page").description("Page of components"),
parameterWithName("page_entries").description("Amount of components per page"),
parameterWithName("sort").description("Defines order of the components")
),
links(
linkWithRel("curies").description("Curies are used for online documentation"),
linkWithRel("first").description("Link to first page"),
linkWithRel("last").description("Link to last page")
),
responseFields(
subsectionWithPath("_embedded.sw360:components.[]name").description("The name of the component"),
subsectionWithPath("_embedded.sw360:components.[]componentType").description("The component type, possible values are: " + Arrays.asList(ComponentType.values())),
subsectionWithPath("_embedded.sw360:components").description("An array of <<resources-components, Components resources>>"),
subsectionWithPath("_links").description("<<resources-index-links,Links>> to other resources"),
fieldWithPath("page").description("Additional paging information"),
fieldWithPath("page.size").description("Number of components per page"),
fieldWithPath("page.totalElements").description("Total number of all existing components"),
fieldWithPath("page.totalPages").description("Total number of pages"),
fieldWithPath("page.number").description("Number of the current page")
)));
}

@Test
public void should_document_update_component() throws Exception {
ComponentDTO updateComponent = new ComponentDTO();
Expand Down
Loading