From 8cb56c0e1e0a44807e5bebd2dd82ef233390f2eb Mon Sep 17 00:00:00 2001 From: Keerthi B L Date: Mon, 23 Dec 2024 15:04:14 +0530 Subject: [PATCH] feat(rest) : Advanced Search for project page Signed-off-by: Keerthi B L --- .../src/docs/asciidoc/projects.adoc | 19 +++ .../project/ProjectController.java | 144 +++++++++++++----- .../project/Sw360ProjectService.java | 5 + .../integration/ProjectTest.java | 1 + .../restdocs/ProjectSpecTest.java | 46 +++++- 5 files changed, 172 insertions(+), 43 deletions(-) diff --git a/rest/resource-server/src/docs/asciidoc/projects.adoc b/rest/resource-server/src/docs/asciidoc/projects.adoc index b7212f92cf..e6b7a332c4 100644 --- a/rest/resource-server/src/docs/asciidoc/projects.adoc +++ b/rest/resource-server/src/docs/asciidoc/projects.adoc @@ -1233,3 +1233,22 @@ include::{snippets}/should_document_get_export_project_create_clearing_request/c ===== Example response include::{snippets}/should_document_get_export_project_create_clearing_request/http-response.adoc[] + +[[resources-projects-list-by-search]] +==== Filtering with more fields + +A `GET` request to fetch filtered list of projects. + +Note : send query parameter's value in encoded format. (Reference: `https://datatracker.ietf.org/doc/html/rfc3986`) + +===== Response structure +include::{snippets}/should_document_get_projects_by_advance_search/response-fields.adoc[] + +===== Example request +include::{snippets}/should_document_get_projects_by_advance_search/curl-request.adoc[] + +===== Example response +include::{snippets}/should_document_get_projects_by_advance_search/http-response.adoc[] + +===== Links +include::{snippets}/should_document_get_projects_by_advance_search/links.adoc[] \ No newline at end of file diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java index a0648184e4..44dc3fefba 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/ProjectController.java @@ -69,11 +69,7 @@ import org.eclipse.sw360.datahandler.thrift.attachments.AttachmentUsage; import org.eclipse.sw360.datahandler.thrift.attachments.CheckStatus; import org.eclipse.sw360.datahandler.thrift.attachments.UsageData; -import org.eclipse.sw360.datahandler.thrift.components.ClearingState; -import org.eclipse.sw360.datahandler.thrift.components.Release; -import org.eclipse.sw360.datahandler.thrift.components.ReleaseClearingStateSummary; -import org.eclipse.sw360.datahandler.thrift.components.ReleaseLink; -import org.eclipse.sw360.datahandler.thrift.components.ReleaseNode; +import org.eclipse.sw360.datahandler.thrift.components.*; import org.eclipse.sw360.datahandler.thrift.licenseinfo.LicenseInfo; import org.eclipse.sw360.datahandler.thrift.licenseinfo.LicenseInfoFile; import org.eclipse.sw360.datahandler.thrift.licenseinfo.LicenseInfoParsingResult; @@ -81,15 +77,7 @@ import org.eclipse.sw360.datahandler.thrift.licenseinfo.OutputFormatInfo; import org.eclipse.sw360.datahandler.thrift.licenseinfo.OutputFormatVariant; import org.eclipse.sw360.datahandler.thrift.licenses.License; -import org.eclipse.sw360.datahandler.thrift.projects.ObligationList; -import org.eclipse.sw360.datahandler.thrift.projects.ObligationStatusInfo; -import org.eclipse.sw360.datahandler.thrift.projects.Project; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectClearingState; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectLink; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectProjectRelationship; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectRelationship; -import org.eclipse.sw360.datahandler.thrift.projects.ProjectDTO; -import org.eclipse.sw360.datahandler.thrift.projects.ClearingRequest; +import org.eclipse.sw360.datahandler.thrift.projects.*; import org.eclipse.sw360.datahandler.thrift.users.User; import org.eclipse.sw360.datahandler.thrift.users.UserGroup; import org.eclipse.sw360.datahandler.thrift.vendors.Vendor; @@ -259,6 +247,16 @@ public ResponseEntity>> getProjectsForUser( @RequestParam(value = "tag", required = false) String tag, @Parameter(description = "Flag to get projects with all details.") @RequestParam(value = "allDetails", required = false) boolean allDetails, + @Parameter(description = "The version of the project") + @RequestParam(value = "version", required = false) String version, + @Parameter(description = "The projectResponsible of the project") + @RequestParam(value = "projectResponsible", required = false) String projectResponsible, + @Parameter(description = "The state of the project") + @RequestParam(value = "state", required = false) ProjectState projectState, + @Parameter(description = "The clearingStatus of the project") + @RequestParam(value = "clearingStatus", required = false) ProjectClearingState projectClearingState, + @Parameter(description = "The additionalData of the project") + @RequestParam(value = "additionalData", required = false) String additionalData, @Parameter(description = "List project by lucene search") @RequestParam(value = "luceneSearch", required = false) boolean luceneSearch, HttpServletRequest request) throws TException, URISyntaxException, PaginationParameterException, ResourceClassNotFoundException { @@ -269,23 +267,13 @@ public ResponseEntity>> getProjectsForUser( boolean isSearchByType = CommonUtils.isNotNullEmptyOrWhitespace(projectType); boolean isSearchByGroup = CommonUtils.isNotNullEmptyOrWhitespace(group); boolean isNoFilter = false; + boolean isAllProjectAdded=false; String queryString = request.getQueryString(); Map params = restControllerHelper.parseQueryString(queryString); List sw360Projects = new ArrayList<>(); - Map> filterMap = new HashMap<>(); if (luceneSearch) { - if (CommonUtils.isNotNullEmptyOrWhitespace(projectType)) { - Set values = CommonUtils.splitToSet(projectType); - filterMap.put(Project._Fields.PROJECT_TYPE.getFieldName(), values); - } - if (CommonUtils.isNotNullEmptyOrWhitespace(group)) { - Set values = CommonUtils.splitToSet(group); - filterMap.put(Project._Fields.BUSINESS_UNIT.getFieldName(), values); - } - if (CommonUtils.isNotNullEmptyOrWhitespace(tag)) { - Set values = CommonUtils.splitToSet(tag); - filterMap.put(Project._Fields.TAG.getFieldName(), values); - } + Map> filterMap = getFilterMap(tag, projectType, group, version, projectResponsible, projectState, projectClearingState, + additionalData); if (CommonUtils.isNotNullEmptyOrWhitespace(name)) { Set values = CommonUtils.splitToSet(name); @@ -298,24 +286,56 @@ public ResponseEntity>> getProjectsForUser( } else { if (isSearchByName) { sw360Projects.addAll(projectService.searchProjectByName(params.get("name"), sw360User)); - } else if (isSearchByGroup) { - sw360Projects.addAll(projectService.searchProjectByGroup(group, sw360User)); - } else if (isSearchByTag) { - sw360Projects.addAll(projectService.searchProjectByTag(params.get("tag"), sw360User)); - } else if (isSearchByType) { - sw360Projects.addAll(projectService.searchProjectByType(projectType, sw360User)); } else { - sw360Projects.addAll(projectService.getProjectsForUser(sw360User, pageable)); + isAllProjectAdded=true; + sw360Projects.addAll(projectService.getProjectsSummaryForUserWithoutPagination(sw360User)); + } + Map> restrictions = getFilterMap(tag, projectType, group, version, projectResponsible, projectState, projectClearingState, + additionalData); + if (!restrictions.isEmpty()) { + sw360Projects = new ArrayList<>(sw360Projects.stream() + .filter(filterProjectMap(restrictions)).toList()); + }else if(isAllProjectAdded){ isNoFilter = true; } } - return getProjectResponse(pageable, projectType, group, tag, allDetails, luceneSearch, request, sw360User, + return getProjectResponse(pageable, allDetails, luceneSearch, request, sw360User, mapOfProjects, isSearchByName, sw360Projects, isNoFilter); } + private Map> getFilterMap(String tag, String projectType, String group, String version, String projectResponsible, + ProjectState projectState, ProjectClearingState projectClearingState, String additionalData) { + Map> filterMap = new HashMap<>(); + if (CommonUtils.isNotNullEmptyOrWhitespace(tag)) { + filterMap.put(Project._Fields.TAG.getFieldName(), CommonUtils.splitToSet(tag)); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(projectType)) { + filterMap.put(Project._Fields.PROJECT_TYPE.getFieldName(), CommonUtils.splitToSet(projectType)); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(group)) { + filterMap.put(Project._Fields.BUSINESS_UNIT.getFieldName(), CommonUtils.splitToSet(group)); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(version)) { + filterMap.put(Project._Fields.VERSION.getFieldName(), CommonUtils.splitToSet(version)); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(projectResponsible)) { + filterMap.put(Project._Fields.PROJECT_RESPONSIBLE.getFieldName(), CommonUtils.splitToSet(projectResponsible)); + } + if (projectState!=null && CommonUtils.isNotNullEmptyOrWhitespace(projectState.name())) { + filterMap.put(Project._Fields.STATE.getFieldName(), CommonUtils.splitToSet(projectState.name())); + } + if (projectClearingState!=null && CommonUtils.isNotNullEmptyOrWhitespace(projectClearingState.name())) { + filterMap.put(Project._Fields.CLEARING_STATE.getFieldName(), CommonUtils.splitToSet(projectClearingState.name())); + } + if (CommonUtils.isNotNullEmptyOrWhitespace(additionalData)) { + filterMap.put(Project._Fields.ADDITIONAL_DATA.getFieldName(), CommonUtils.splitToSet(additionalData)); + } + return filterMap; + } + @NotNull private ResponseEntity>> getProjectResponse(Pageable pageable, - String projectType, String group, String tag, boolean allDetails, boolean luceneSearch, + boolean allDetails, boolean luceneSearch, HttpServletRequest request, User sw360User, Map mapOfProjects, boolean isSearchByName, List sw360Projects, boolean isNoFilter) throws ResourceClassNotFoundException, PaginationParameterException, URISyntaxException, TException { sw360Projects.stream().forEach(prj -> mapOfProjects.put(prj.getId(), prj)); @@ -347,11 +367,9 @@ private ResponseEntity>> getProjectResponse if (luceneSearch) { paginationResult.getResources().stream().forEach(consumer); } else { - paginationResult.getResources().stream() - .filter(project -> projectType == null || projectType.equals(project.projectType.name())) - .filter(project -> group == null || group.isEmpty() || group.equals(project.getBusinessUnit())) - .filter(project -> tag == null || tag.isEmpty() || tag.equals(project.getTag())).forEach(consumer); + paginationResult.getResources().stream().forEach(consumer); } + CollectionModel resources; if (projectResources.size() == 0) { resources = restControllerHelper.emptyPageResource(Project.class, paginationResult); @@ -415,7 +433,7 @@ public ResponseEntity>> getProjectsFiltered List sw360Projects = projectService.getMyProjects(sw360User, userRoles); sw360Projects = projectService.getWithFilledClearingStatus(sw360Projects, clearingState); - return getProjectResponse(pageable, null, null, null, allDetails, true, request, sw360User, + return getProjectResponse(pageable, allDetails, true, request, sw360User, mapOfProjects, true, sw360Projects, false); } @@ -3396,4 +3414,48 @@ public ResponseEntity createDuplicateProjectWithDependencyNetwork( return ResponseEntity.created(location).body(projectDTOHalResource); } + + /** + * Create a filter predicate to remove all projects which do not satisfy the restriction set. + * @param restrictions Restrictions set to filter projects on + * @return Filter predicate for stream. + */ + private static @NonNull Predicate filterProjectMap(Map> restrictions) { + return project -> { + for (Map.Entry> restriction : restrictions.entrySet()) { + final Set filterSet = restriction.getValue(); + Project._Fields field = Project._Fields.findByName(restriction.getKey()); + Object fieldValue = project.getFieldValue(field); + if (fieldValue == null) { + return false; + } + if (field == Project._Fields.PROJECT_TYPE && !filterSet.contains(project.projectType.name())) { + return false; + } else if (field == Project._Fields.VERSION && !filterSet.contains(project.version)) { + return false; + } else if (field == Project._Fields.PROJECT_RESPONSIBLE && !filterSet.contains(project.projectResponsible)) { + return false; + } else if (field == Project._Fields.STATE && !filterSet.contains(project.state.name())) { + return false; + } else if (field == Project._Fields.CLEARING_STATE && !filterSet.contains(project.clearingState.name())) { + return false; + } else if ((field == Project._Fields.CREATED_BY || field == Project._Fields.CREATED_ON) + && !fieldValue.toString().equalsIgnoreCase(filterSet.iterator().next())) { + return false; + } else if (fieldValue instanceof Set) { + if (Sets.intersection(filterSet, (Set) fieldValue).isEmpty()) { + return false; + } + } else if (fieldValue instanceof Map) { + Map fieldValueMap = (Map) fieldValue; + boolean hasIntersection = fieldValueMap.keySet().stream() + .anyMatch(filterSet::contains); + if (!hasIntersection) { + return false; + } + } + } + return true; + }; + } } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java index dc577c1ccb..79881113c8 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/project/Sw360ProjectService.java @@ -145,6 +145,11 @@ public Set getProjectsForUser(User sw360User, Pageable pageable) throws return new HashSet<>(pageDtToProjects.entrySet().iterator().next().getValue()); } + public List getProjectsSummaryForUserWithoutPagination(User sw360User) throws TException { + ProjectService.Iface sw360ProjectClient = getThriftProjectClient(); + return sw360ProjectClient.getAccessibleProjectsSummary(sw360User); + } + public Project getProjectForUserById(String projectId, User sw360User) throws TException { ProjectService.Iface sw360ProjectClient = getThriftProjectClient(); try { diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/integration/ProjectTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/integration/ProjectTest.java index 5c9d232085..0f755a9c93 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/integration/ProjectTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/integration/ProjectTest.java @@ -51,6 +51,7 @@ public void before() throws TException { projectList.add(project); given(this.projectServiceMock.getProjectsForUser(any(), any())).willReturn(projectList); + given(this.projectServiceMock.getProjectsSummaryForUserWithoutPagination(any())).willReturn(projectList.stream().toList()); User user = new User(); user.setId("123456789"); diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java index c7910f8b3a..ec5fe57463 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ProjectSpecTest.java @@ -582,6 +582,7 @@ public void before() throws TException, IOException { given(this.sw360ReportServiceMock.getProjectReleaseSpreadSheetWithEcc(any(),any())).willReturn(ByteBuffer.allocate(10000)); given(this.projectServiceMock.getProjectsForUser(any(), any())).willReturn(projectList); given(this.projectServiceMock.getProjectForUserById(eq(project.getId()), any())).willReturn(project); + given(this.projectServiceMock.getProjectsSummaryForUserWithoutPagination(any())).willReturn(projectList.stream().toList()); given(this.projectServiceMock.getProjectForUserById(eq(project2.getId()), any())).willReturn(project2); given(this.projectServiceMock.getProjectForUserById(eq(project4.getId()), any())).willReturn(project4); given(this.projectServiceMock.getProjectForUserById(eq(project5.getId()), any())).willReturn(project5); @@ -1035,13 +1036,13 @@ public void should_document_get_projects_with_all_details() throws Exception { subsectionWithPath("_embedded.sw360:projects.[]clearingRequestId").description("Clearing Request id associated with project."), subsectionWithPath("_embedded.sw360:projects.[]_links").description("Self <> to Project resource"), subsectionWithPath("_embedded.sw360:projects.[]_embedded.createdBy").description("The user who created this project"), - subsectionWithPath("_embedded.sw360:projects.[]_embedded.clearingTeam").description("The clearingTeam of the project").optional(), + subsectionWithPath("_embedded.sw360:projects.[]_embedded.clearingTeam").type(JsonFieldType.STRING).description("The clearingTeam of the project").optional(), subsectionWithPath("_embedded.sw360:projects.[]_embedded.homepage").description("The homepage url of the project").optional(), subsectionWithPath("_embedded.sw360:projects.[]_embedded.wiki").description("The wiki url of the project").optional(), subsectionWithPath("_embedded.sw360:projects.[]licenseInfoHeaderText").description("The licenseInfoHeaderText text of the project"), subsectionWithPath("_embedded.sw360:projects.[]externalUrls").description("A place to store additional data used by external tools").optional(), subsectionWithPath("_embedded.sw360:projects.[]_embedded.sw360:moderators").description("An array of all project moderators with email").optional(), - subsectionWithPath("_embedded.sw360:projects.[]_embedded.sw360:contributors").description("An array of all project contributors with email").optional(), + subsectionWithPath("_embedded.sw360:projects.[]_embedded.sw360:contributors").type(JsonFieldType.ARRAY).description("An array of all project contributors with email").optional(), subsectionWithPath("_embedded.sw360:projects.[]_embedded.sw360:attachments").description("An array of all project attachments").optional(), subsectionWithPath("_embedded.sw360:projects.[]vendor").description("An array of all component vendors with full name and link to their <>"), subsectionWithPath("_links").description("<> to other resources"), @@ -3200,4 +3201,45 @@ public void should_document_get_project_release_with_ecc_spreadsheet() throws Ex parameterWithName("projectId").description("Id of a project")) )); } + + @Test + public void should_document_get_projects_by_advance_search() throws Exception { + mockMvc.perform(get("/api/projects") + .header("Authorization", TestHelper.generateAuthHeader(testUserId, testUserPassword)) + .queryParam("projectType", project.getProjectType().toString()) + .queryParam("createdOn", project.getCreatedOn()) + .queryParam("version", project.getVersion()) + .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("projectType").description("Filter for type"), + parameterWithName("createdOn").description("Filter for project creation date"), + parameterWithName("version").description("Filter for version"), + parameterWithName("luceneSearch").description("Filter with exact match or lucene match."), + parameterWithName("page").description("Page of projects"), + parameterWithName("page_entries").description("Amount of projects per page"), + parameterWithName("sort").description("Defines order of the projects") + ), + 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:projects.[]name").description("The name of the component"), + subsectionWithPath("_embedded.sw360:projects.[]projectType").description("The component type, possible values are: " + Arrays.asList(ComponentType.values())), + subsectionWithPath("_embedded.sw360:projects").description("An array of <>"), + subsectionWithPath("_links").description("<> to other resources"), + fieldWithPath("page").description("Additional paging information"), + fieldWithPath("page.size").description("Number of projects per page"), + fieldWithPath("page.totalElements").description("Total number of all existing projects"), + fieldWithPath("page.totalPages").description("Total number of pages"), + fieldWithPath("page.number").description("Number of the current page") + ))); + } }