From b9d62a20581fef52de8390af82700d4ebfa834a1 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Mon, 28 Oct 2019 09:32:38 +0100 Subject: [PATCH] Added logic for generic search functionality --- .../controller/GroupsTabResponseBuilder.java | 4 +- .../ia2/gms/controller/SearchController.java | 39 +++++ .../model/response/SearchResponseItem.java | 32 +++++ .../model/response/SearchResponseType.java | 7 + .../model/response/UserSearchResponse.java | 27 ++++ .../inaf/ia2/gms/persistence/GroupsDAO.java | 13 ++ .../inaf/ia2/gms/service/GroupsService.java | 4 + .../ia2/gms/service/GroupsTreeBuilder.java | 3 + .../inaf/ia2/gms/service/SearchService.java | 120 ++++++++++++++++ .../gms/controller/SearchControllerTest.java | 76 ++++++++++ .../ia2/gms/service/SearchServiceTest.java | 133 ++++++++++++++++++ 11 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseItem.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseType.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java create mode 100644 gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java create mode 100644 gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java create mode 100644 gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java index fb1359a..0b0dd8a 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java @@ -24,7 +24,7 @@ public class GroupsTabResponseBuilder { private GroupsService groupsService; @Autowired - private GroupsTreeBuilder groupsTreeBuilder; + private GroupsTreeBuilder groupsListBuilder; public GroupsTabResponse getGroupsTab(GroupsRequest request) { @@ -37,7 +37,7 @@ public class GroupsTabResponseBuilder { Permission currentNodePermissions = permissionsService.getUserPermissionForGroup(group, session.getUserId()); response.setPermission(currentNodePermissions); - response.setGroupsPanel(groupsTreeBuilder.listSubGroups(group, request, session.getUserId())); + response.setGroupsPanel(groupsListBuilder.listSubGroups(group, request, session.getUserId())); return response; } diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java new file mode 100644 index 0000000..6920999 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java @@ -0,0 +1,39 @@ +package it.inaf.ia2.gms.controller; + +import it.inaf.ia2.gms.authn.SessionData; +import it.inaf.ia2.gms.model.response.PaginatedData; +import it.inaf.ia2.gms.model.response.SearchResponseItem; +import it.inaf.ia2.gms.model.response.UserSearchResponse; +import it.inaf.ia2.gms.service.SearchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SearchController { + + @Autowired + private SessionData sessionData; + + @Autowired + private SearchService searchService; + + @GetMapping(value = "/search", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity<PaginatedData<SearchResponseItem>> getSearchResults(@RequestParam("query") String query, + @RequestParam("page") int page, @RequestParam("pageSize") int pageSize) { + + PaginatedData<SearchResponseItem> response = searchService.search(query, sessionData.getUserId(), page, pageSize); + return ResponseEntity.ok(response); + } + + @GetMapping(value = "/search/user/{userId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity<UserSearchResponse> getSearchResultUser(@PathVariable("userId") String userId) { + + UserSearchResponse response = searchService.getUserSearchResult(sessionData.getUserId(), userId); + return ResponseEntity.ok(response); + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseItem.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseItem.java new file mode 100644 index 0000000..9eff1e1 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseItem.java @@ -0,0 +1,32 @@ +package it.inaf.ia2.gms.model.response; + +public class SearchResponseItem { + + private SearchResponseType type; + private String label; + private String id; + + public SearchResponseType getType() { + return type; + } + + public void setType(SearchResponseType type) { + this.type = type; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseType.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseType.java new file mode 100644 index 0000000..e9e2bc8 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/SearchResponseType.java @@ -0,0 +1,7 @@ +package it.inaf.ia2.gms.model.response; + +public enum SearchResponseType { + + GROUP, + USER +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java new file mode 100644 index 0000000..d09cc2b --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java @@ -0,0 +1,27 @@ +package it.inaf.ia2.gms.model.response; + +import it.inaf.ia2.gms.persistence.model.GroupEntity; +import it.inaf.ia2.gms.persistence.model.PermissionEntity; +import java.util.List; + +public class UserSearchResponse { + + private List<GroupEntity> groups; + private List<PermissionEntity> permissions; + + public List<GroupEntity> getGroups() { + return groups; + } + + public void setGroups(List<GroupEntity> groups) { + this.groups = groups; + } + + public List<PermissionEntity> getPermissions() { + return permissions; + } + + public void setPermissions(List<PermissionEntity> permissions) { + this.permissions = permissions; + } +} diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java index 84a2654..68cf07e 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java +++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java @@ -299,4 +299,17 @@ public class GroupsDAO { return breadcrumbs; }); } + + public List<GroupEntity> searchGroups(String searchText) { + + String sql = "SELECT id, name, path from gms_group WHERE name ILIKE ?"; + + return jdbcTemplate.query(conn -> { + PreparedStatement ps = conn.prepareStatement(sql); + ps.setString(1, "%" + searchText + "%"); + return ps; + }, resultSet -> { + return getGroupsFromResultSet(resultSet); + }); + } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java index 609a787..8ea1979 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java +++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java @@ -160,4 +160,8 @@ public class GroupsService { } return null; } + + public List<GroupEntity> searchGroups(String searchText) { + return groupsDAO.searchGroups(searchText); + } } diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java index b56abf6..9da1f20 100644 --- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java +++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java @@ -16,6 +16,9 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +/** + * Returns to the user only the groups he or she is allowed to see. + */ @Component public class GroupsTreeBuilder { diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java b/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java new file mode 100644 index 0000000..f809a61 --- /dev/null +++ b/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java @@ -0,0 +1,120 @@ +package it.inaf.ia2.gms.service; + +import it.inaf.ia2.gms.model.Permission; +import it.inaf.ia2.gms.model.response.PaginatedData; +import it.inaf.ia2.gms.model.response.SearchResponseItem; +import it.inaf.ia2.gms.model.response.SearchResponseType; +import it.inaf.ia2.gms.model.response.UserSearchResponse; +import it.inaf.ia2.gms.persistence.GroupsDAO; +import it.inaf.ia2.gms.persistence.MembershipsDAO; +import it.inaf.ia2.gms.persistence.PermissionsDAO; +import it.inaf.ia2.gms.persistence.model.GroupEntity; +import it.inaf.ia2.gms.persistence.model.PermissionEntity; +import it.inaf.ia2.gms.rap.RapClient; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class SearchService { + + @Autowired + private RapClient rapClient; + + @Autowired + private GroupsService groupsService; + + @Autowired + private GroupsDAO groupsDAO; + + @Autowired + private PermissionsDAO permissionsDAO; + + @Autowired + private MembershipsDAO membershipsDAO; + + /** + * Generic search (both groups and users). + */ + public PaginatedData<SearchResponseItem> search(String query, String userId, int page, int pageSize) { + + List<SearchResponseItem> items = searchUsers(query); + items.addAll(searchGroups(query, userId)); + + // sort by label + items.sort((i1, i2) -> i1.getLabel().compareTo(i2.getLabel())); + + return new PaginatedData<>(items, page, pageSize); + } + + private List<SearchResponseItem> searchUsers(String query) { + return rapClient.searchUsers(query).stream() + .map(u -> { + SearchResponseItem item = new SearchResponseItem(); + item.setType(SearchResponseType.USER); + item.setId(u.getId()); + item.setLabel(u.getDisplayName()); + return item; + }) + .collect(Collectors.toList()); + } + + private List<SearchResponseItem> searchGroups(String query, String userId) { + + List<GroupEntity> allGroups = groupsDAO.searchGroups(query); + + // Select only the groups visible to the user + List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(userId); + + List<SearchResponseItem> items = new ArrayList<>(); + for (GroupEntity group : allGroups) { + + PermissionUtils.getGroupPermission(group, permissions).ifPresent(permission -> { + SearchResponseItem item = new SearchResponseItem(); + item.setType(SearchResponseType.GROUP); + item.setId(group.getId()); + item.setLabel(group.getName()); + items.add(item); + }); + } + + return items; + } + + /** + * Retrieve additional data about an user displayed into the generic search. + * + * @param actorUserId the user who performed the search + * @param targetUserId the user displayed into the search results + */ + public UserSearchResponse getUserSearchResult(String actorUserId, String targetUserId) { + + List<GroupEntity> allGroups = membershipsDAO.getUserMemberships(targetUserId); + + // Select only the groups visible to the user + List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(actorUserId); + + List<GroupEntity> visibleGroups = new ArrayList<>(); + for (GroupEntity group : allGroups) { + + PermissionUtils.getGroupPermission(group, permissions).ifPresent(permission -> { + visibleGroups.add(group); + }); + } + + UserSearchResponse response = new UserSearchResponse(); + response.setGroups(visibleGroups); + + // Super-admin user is able to see also other user permissions + PermissionUtils.getGroupPermission(groupsService.getRoot(), permissions).ifPresent(permission -> { + if (permission.equals(Permission.ADMIN)) { + List<PermissionEntity> targetUserPermissions = permissionsDAO.findUserPermissions(targetUserId); + response.setPermissions(targetUserPermissions); + } + }); + + return response; + } +} diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java new file mode 100644 index 0000000..f419eba --- /dev/null +++ b/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java @@ -0,0 +1,76 @@ +package it.inaf.ia2.gms.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.inaf.ia2.gms.authn.SessionData; +import it.inaf.ia2.gms.model.response.PaginatedData; +import it.inaf.ia2.gms.model.response.SearchResponseItem; +import it.inaf.ia2.gms.model.response.UserSearchResponse; +import it.inaf.ia2.gms.service.SearchService; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@RunWith(MockitoJUnitRunner.class) +public class SearchControllerTest { + + @Mock + private SessionData session; + + @Mock + private SearchService searchService; + + @InjectMocks + private SearchController controller; + + private MockMvc mockMvc; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Before + public void init() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + + when(session.getUserId()).thenReturn("admin_id"); + } + + @Test + public void testGenericSearch() throws Exception { + + PaginatedData<SearchResponseItem> response = new PaginatedData<>(new ArrayList<>(), 1, 10); + + when(searchService.search(any(), any(), anyInt(), anyInt())).thenReturn(response); + + mockMvc.perform(get("/search?query=searchText&page=1&pageSize=10") + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()); + + verify(searchService, times(1)).search(eq("searchText"), eq("admin_id"), eq(1), eq(10)); + } + + @Test + public void testUserSearch() throws Exception { + + when(searchService.getUserSearchResult(any(), any())).thenReturn(new UserSearchResponse()); + + mockMvc.perform(get("/search/user/user_id") + .contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()); + + verify(searchService, times(1)).getUserSearchResult(eq("admin_id"), eq("user_id")); + } +} diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java new file mode 100644 index 0000000..7d11078 --- /dev/null +++ b/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java @@ -0,0 +1,133 @@ +package it.inaf.ia2.gms.service; + +import it.inaf.ia2.gms.model.Identity; +import it.inaf.ia2.gms.model.IdentityType; +import it.inaf.ia2.gms.model.Permission; +import it.inaf.ia2.gms.model.RapUser; +import it.inaf.ia2.gms.model.response.PaginatedData; +import it.inaf.ia2.gms.model.response.SearchResponseItem; +import it.inaf.ia2.gms.model.response.SearchResponseType; +import it.inaf.ia2.gms.model.response.UserSearchResponse; +import it.inaf.ia2.gms.persistence.GroupsDAO; +import it.inaf.ia2.gms.persistence.MembershipsDAO; +import it.inaf.ia2.gms.persistence.PermissionsDAO; +import it.inaf.ia2.gms.persistence.model.GroupEntity; +import it.inaf.ia2.gms.persistence.model.PermissionEntity; +import it.inaf.ia2.gms.rap.RapClient; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SearchServiceTest { + + @Mock + private RapClient rapClient; + + @Mock + private GroupsService groupsService; + + @Mock + private GroupsDAO groupsDAO; + + @Mock + private PermissionsDAO permissionsDAO; + + @Mock + private MembershipsDAO membershipsDAO; + + @InjectMocks + private SearchService searchService; + + @Test + public void testGenericSearch() { + + RapUser user = new RapUser(); + user.setId("user_id"); + Identity identity = new Identity(); + identity.setPrimary(true); + identity.setType(IdentityType.EDU_GAIN); + identity.setEmail("user@inaf.it"); + user.setIdentities(Collections.singletonList(identity)); + + when(rapClient.searchUsers(any())).thenReturn(Collections.singletonList(user)); + + GroupEntity group1 = new GroupEntity(); + group1.setId("group1_id"); + group1.setName("Group 1"); + group1.setPath("group1_id"); + GroupEntity group2 = new GroupEntity(); + group2.setId("group2_id"); + group2.setName("Group 2"); + group2.setPath("group2_id"); + List<GroupEntity> allGroups = new ArrayList<>(); + allGroups.add(group1); + allGroups.add(group2); + + when(groupsDAO.searchGroups(any())).thenReturn(allGroups); + + PermissionEntity permission = new PermissionEntity(); + permission.setGroupId("group1_id"); + permission.setUserId("manager_id"); + permission.setGroupPath("group1_id"); + permission.setPermission(Permission.MANAGE_MEMBERS); + + when(permissionsDAO.findUserPermissions(any())).thenReturn( + Collections.singletonList(permission)); + + PaginatedData<SearchResponseItem> response = searchService.search("foo", "manager_id", 1, 10); + + assertEquals(2, response.getTotalItems()); + + SearchResponseItem item0 = response.getItems().get(0); + assertEquals(SearchResponseType.GROUP, item0.getType()); + assertEquals("group1_id", item0.getId()); + assertEquals("Group 1", item0.getLabel()); + + SearchResponseItem item1 = response.getItems().get(1); + assertEquals(SearchResponseType.USER, item1.getType()); + assertEquals("user_id", item1.getId()); + assertEquals("user@inaf.it (EduGAIN)", item1.getLabel()); + } + + @Test + public void testGetUserSearchResult() { + + GroupEntity group1 = new GroupEntity(); + group1.setId("group1_id"); + group1.setName("Group 1"); + group1.setPath("group1_id"); + + when(membershipsDAO.getUserMemberships(any())) + .thenReturn(Collections.singletonList(group1)); + + PermissionEntity adminPermission = new PermissionEntity(); + adminPermission.setGroupId("ROOT"); + adminPermission.setUserId("admin_id"); + adminPermission.setGroupPath(""); + adminPermission.setPermission(Permission.ADMIN); + + when(permissionsDAO.findUserPermissions(eq("admin_id"))) + .thenReturn(Collections.singletonList(adminPermission)); + + GroupEntity root = new GroupEntity(); + root.setId("ROOT"); + root.setName("Root"); + root.setPath(""); + when(groupsService.getRoot()).thenReturn(root); + + UserSearchResponse response = searchService.getUserSearchResult("admin_id", "target_id"); + assertEquals(1, response.getGroups().size()); + assertNotNull(response.getPermissions()); + } +} -- GitLab