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