Merge pull request #9197 from DSpace/backport-9125-to-dspace-7_x

[Port dspace-7_x] Add `isNotMemberOf` searches for Groups and EPersons (for improved performance on Edit Group pages)
This commit is contained in:
Tim Donohue
2023-11-10 15:14:34 -06:00
committed by GitHub
15 changed files with 1194 additions and 108 deletions

View File

@@ -83,13 +83,14 @@ public abstract class AbstractHibernateDSODAO<T extends DSpaceObject> extends Ab
if (CollectionUtils.isNotEmpty(metadataFields) || StringUtils.isNotBlank(additionalWhere)) {
//Add the where query on metadata
query.append(" WHERE ");
// Group the 'OR' clauses below in outer parentheses, e.g. "WHERE (clause1 OR clause2 OR clause3)".
// Grouping these 'OR' clauses allows for later code to append 'AND' clauses without unexpected behaviors
query.append("(");
for (int i = 0; i < metadataFields.size(); i++) {
MetadataField metadataField = metadataFields.get(i);
if (StringUtils.isNotBlank(operator)) {
query.append(" (");
query.append("lower(STR(" + metadataField.toString()).append(".value)) ").append(operator)
.append(" lower(:queryParam)");
query.append(")");
if (i < metadataFields.size() - 1) {
query.append(" OR ");
}
@@ -102,6 +103,7 @@ public abstract class AbstractHibernateDSODAO<T extends DSpaceObject> extends Ab
}
query.append(additionalWhere);
}
query.append(")");
}
}

View File

@@ -184,32 +184,98 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl<EPerson> impleme
@Override
public List<EPerson> search(Context context, String query, int offset, int limit) throws SQLException {
try {
List<EPerson> ePerson = new ArrayList<>();
EPerson person = find(context, UUID.fromString(query));
if (person != null) {
ePerson.add(person);
}
return ePerson;
} catch (IllegalArgumentException e) {
List<EPerson> ePersons = new ArrayList<>();
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Search by firstname & lastname (NOTE: email will also be included automatically)
MetadataField firstNameField = metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameField = metadataFieldService.findByElement(context, "eperson", "lastname", null);
if (StringUtils.isBlank(query)) {
query = null;
}
return ePersonDAO.search(context, query, Arrays.asList(firstNameField, lastNameField),
ePersons = ePersonDAO.search(context, query, Arrays.asList(firstNameField, lastNameField),
Arrays.asList(firstNameField, lastNameField), offset, limit);
} else {
// Search by UUID
EPerson person = find(context, uuid);
if (person != null) {
ePersons.add(person);
}
}
return ePersons;
}
@Override
public int searchResultCount(Context context, String query) throws SQLException {
int result = 0;
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Count results found by firstname & lastname (email is also included automatically)
MetadataField firstNameField = metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameField = metadataFieldService.findByElement(context, "eperson", "lastname", null);
if (StringUtils.isBlank(query)) {
query = null;
}
return ePersonDAO.searchResultCount(context, query, Arrays.asList(firstNameField, lastNameField));
result = ePersonDAO.searchResultCount(context, query, Arrays.asList(firstNameField, lastNameField));
} else {
// Search by UUID
EPerson person = find(context, uuid);
if (person != null) {
result = 1;
}
}
return result;
}
@Override
public List<EPerson> searchNonMembers(Context context, String query, Group excludeGroup, int offset, int limit)
throws SQLException {
List<EPerson> ePersons = new ArrayList<>();
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Search by firstname & lastname (NOTE: email will also be included automatically)
MetadataField firstNameField = metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameField = metadataFieldService.findByElement(context, "eperson", "lastname", null);
if (StringUtils.isBlank(query)) {
query = null;
}
ePersons = ePersonDAO.searchNotMember(context, query, Arrays.asList(firstNameField, lastNameField),
excludeGroup, Arrays.asList(firstNameField, lastNameField),
offset, limit);
} else {
// Search by UUID
EPerson person = find(context, uuid);
// Verify EPerson is NOT a member of the given excludeGroup before adding
if (person != null && !groupService.isDirectMember(excludeGroup, person)) {
ePersons.add(person);
}
}
return ePersons;
}
@Override
public int searchNonMembersCount(Context context, String query, Group excludeGroup) throws SQLException {
int result = 0;
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Count results found by firstname & lastname (email is also included automatically)
MetadataField firstNameField = metadataFieldService.findByElement(context, "eperson", "firstname", null);
MetadataField lastNameField = metadataFieldService.findByElement(context, "eperson", "lastname", null);
if (StringUtils.isBlank(query)) {
query = null;
}
result = ePersonDAO.searchNotMemberCount(context, query, Arrays.asList(firstNameField, lastNameField),
excludeGroup);
} else {
// Search by UUID
EPerson person = find(context, uuid);
// Verify EPerson is NOT a member of the given excludeGroup before counting
if (person != null && !groupService.isDirectMember(excludeGroup, person)) {
result = 1;
}
}
return result;
}
@Override

View File

@@ -460,17 +460,17 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
}
@Override
public List<Group> search(Context context, String groupIdentifier) throws SQLException {
return search(context, groupIdentifier, -1, -1);
public List<Group> search(Context context, String query) throws SQLException {
return search(context, query, -1, -1);
}
@Override
public List<Group> search(Context context, String groupIdentifier, int offset, int limit) throws SQLException {
public List<Group> search(Context context, String query, int offset, int limit) throws SQLException {
List<Group> groups = new ArrayList<>();
UUID uuid = UUIDUtils.fromString(groupIdentifier);
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
//Search by group name
groups = groupDAO.findByNameLike(context, groupIdentifier, offset, limit);
groups = groupDAO.findByNameLike(context, query, offset, limit);
} else {
//Search by group id
Group group = find(context, uuid);
@@ -483,12 +483,12 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
}
@Override
public int searchResultCount(Context context, String groupIdentifier) throws SQLException {
public int searchResultCount(Context context, String query) throws SQLException {
int result = 0;
UUID uuid = UUIDUtils.fromString(groupIdentifier);
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
//Search by group name
result = groupDAO.countByNameLike(context, groupIdentifier);
result = groupDAO.countByNameLike(context, query);
} else {
//Search by group id
Group group = find(context, uuid);
@@ -500,6 +500,44 @@ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements
return result;
}
@Override
public List<Group> searchNonMembers(Context context, String query, Group excludeParentGroup,
int offset, int limit) throws SQLException {
List<Group> groups = new ArrayList<>();
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Search by group name
groups = groupDAO.findByNameLikeAndNotMember(context, query, excludeParentGroup, offset, limit);
} else if (!uuid.equals(excludeParentGroup.getID())) {
// Search by group id
Group group = find(context, uuid);
// Verify it is NOT a member of the given excludeParentGroup before adding
if (group != null && !isMember(excludeParentGroup, group)) {
groups.add(group);
}
}
return groups;
}
@Override
public int searchNonMembersCount(Context context, String query, Group excludeParentGroup) throws SQLException {
int result = 0;
UUID uuid = UUIDUtils.fromString(query);
if (uuid == null) {
// Search by group name
result = groupDAO.countByNameLikeAndNotMember(context, query, excludeParentGroup);
} else if (!uuid.equals(excludeParentGroup.getID())) {
// Search by group id
Group group = find(context, uuid);
// Verify it is NOT a member of the given excludeParentGroup before adding
if (group != null && !isMember(excludeParentGroup, group)) {
result = 1;
}
}
return result;
}
@Override
public void delete(Context context, Group group) throws SQLException {
if (group.isPermanent()) {

View File

@@ -33,11 +33,68 @@ public interface EPersonDAO extends DSpaceObjectDAO<EPerson>, DSpaceObjectLegacy
public EPerson findByNetid(Context context, String netid) throws SQLException;
/**
* Search all EPersons by the given MetadataField objects, sorting by the given sort fields.
* <P>
* NOTE: As long as a query is specified, the EPerson's email address is included in the search alongside any given
* metadata fields.
*
* @param context DSpace context
* @param query the text to search EPersons for
* @param queryFields the metadata fields to search within (email is also included automatically)
* @param sortFields the metadata field(s) to sort the results by
* @param offset the position of the first result to return
* @param limit how many results return
* @return List of matching EPerson objects
* @throws SQLException if an error occurs
*/
public List<EPerson> search(Context context, String query, List<MetadataField> queryFields,
List<MetadataField> sortFields, int offset, int limit) throws SQLException;
/**
* Count number of EPersons who match a search on the given metadata fields. This returns the count of total
* results for the same query using the 'search()', and therefore can be used to provide pagination.
*
* @param context DSpace context
* @param query the text to search EPersons for
* @param queryFields the metadata fields to search within (email is also included automatically)
* @return total number of EPersons who match the query
* @throws SQLException if an error occurs
*/
public int searchResultCount(Context context, String query, List<MetadataField> queryFields) throws SQLException;
/**
* Search all EPersons via their firstname, lastname, email (fuzzy match), limited to those EPersons which are NOT
* a member of the given group. This may be used to search across EPersons which are valid to add as members to the
* given group.
*
* @param context The DSpace context
* @param query the text to search EPersons for
* @param queryFields the metadata fields to search within (email is also included automatically)
* @param excludeGroup Group to exclude results from. Members of this group will never be returned.
* @param offset the position of the first result to return
* @param limit how many results return
* @return EPersons matching the query (which are not members of the given group)
* @throws SQLException if database error
*/
List<EPerson> searchNotMember(Context context, String query, List<MetadataField> queryFields, Group excludeGroup,
List<MetadataField> sortFields, int offset, int limit) throws SQLException;
/**
* Count number of EPersons that match a given search (fuzzy match) across firstname, lastname and email. This
* search is limited to those EPersons which are NOT a member of the given group. This may be used
* (with searchNotMember()) to perform a paginated search across EPersons which are valid to add to the given group.
*
* @param context The DSpace context
* @param query querystring to fuzzy match against.
* @param queryFields the metadata fields to search within (email is also included automatically)
* @param excludeGroup Group to exclude results from. Members of this group will never be returned.
* @return Groups matching the query (which are not members of the given parent)
* @throws SQLException if database error
*/
int searchNotMemberCount(Context context, String query, List<MetadataField> queryFields, Group excludeGroup)
throws SQLException;
/**
* Find all EPersons who are a member of one or more of the listed groups in a paginated fashion. This returns
* EPersons ordered by UUID.

View File

@@ -135,6 +135,38 @@ public interface GroupDAO extends DSpaceObjectDAO<Group>, DSpaceObjectLegacySupp
*/
int countByNameLike(Context context, String groupName) throws SQLException;
/**
* Search all groups via their name (fuzzy match), limited to those groups which are NOT a member of the given
* parent group. This may be used to search across groups which are valid to add to the given parent group.
* <P>
* NOTE: The parent group itself is also excluded from the search.
*
* @param context The DSpace context
* @param groupName Group name to fuzzy match against.
* @param excludeParent Parent Group to exclude results from. Groups under this parent will never be returned.
* @param offset Offset to use for pagination (-1 to disable)
* @param limit The maximum number of results to return (-1 to disable)
* @return Groups matching the query (which are not members of the given parent)
* @throws SQLException if database error
*/
List<Group> findByNameLikeAndNotMember(Context context, String groupName, Group excludeParent,
int offset, int limit) throws SQLException;
/**
* Count number of groups that match a given name (fuzzy match), limited to those groups which are NOT a member of
* the given parent group. This may be used (with findByNameLikeAndNotMember()) to search across groups which are
* valid to add to the given parent group.
* <P>
* NOTE: The parent group itself is also excluded from the count.
*
* @param context The DSpace context
* @param groupName Group name to fuzzy match against.
* @param excludeParent Parent Group to exclude results from. Groups under this parent will never be returned.
* @return Groups matching the query (which are not members of the given parent)
* @throws SQLException if database error
*/
int countByNameLikeAndNotMember(Context context, String groupName, Group excludeParent) throws SQLException;
/**
* Find a group by its name and the membership of the given EPerson
*

View File

@@ -70,17 +70,9 @@ public class EPersonDAOImpl extends AbstractHibernateDSODAO<EPerson> implements
String queryString = "SELECT " + EPerson.class.getSimpleName()
.toLowerCase() + " FROM EPerson as " + EPerson.class
.getSimpleName().toLowerCase() + " ";
if (query != null) {
query = "%" + query.toLowerCase() + "%";
}
Query hibernateQuery = getSearchQuery(context, queryString, query, queryFields, sortFields, null);
if (0 <= offset) {
hibernateQuery.setFirstResult(offset);
}
if (0 <= limit) {
hibernateQuery.setMaxResults(limit);
}
Query hibernateQuery = getSearchQuery(context, queryString, query, queryFields, null,
sortFields, null, limit, offset);
return list(hibernateQuery);
}
@@ -92,6 +84,28 @@ public class EPersonDAOImpl extends AbstractHibernateDSODAO<EPerson> implements
return count(hibernateQuery);
}
@Override
public List<EPerson> searchNotMember(Context context, String query, List<MetadataField> queryFields,
Group excludeGroup, List<MetadataField> sortFields,
int offset, int limit) throws SQLException {
String queryString = "SELECT " + EPerson.class.getSimpleName()
.toLowerCase() + " FROM EPerson as " + EPerson.class
.getSimpleName().toLowerCase() + " ";
Query hibernateQuery = getSearchQuery(context, queryString, query, queryFields, excludeGroup,
sortFields, null, limit, offset);
return list(hibernateQuery);
}
public int searchNotMemberCount(Context context, String query, List<MetadataField> queryFields,
Group excludeGroup) throws SQLException {
String queryString = "SELECT count(*) FROM EPerson as " + EPerson.class.getSimpleName().toLowerCase();
Query hibernateQuery = getSearchQuery(context, queryString, query, queryFields, excludeGroup,
Collections.EMPTY_LIST, null, -1, -1);
return count(hibernateQuery);
}
@Override
public List<EPerson> findAll(Context context, MetadataField metadataSortField, String sortField, int pageSize,
int offset) throws SQLException {
@@ -105,8 +119,8 @@ public class EPersonDAOImpl extends AbstractHibernateDSODAO<EPerson> implements
sortFields = Collections.singletonList(metadataSortField);
}
Query query = getSearchQuery(context, queryString, null, ListUtils.EMPTY_LIST, sortFields, sortField, pageSize,
offset);
Query query = getSearchQuery(context, queryString, null, ListUtils.EMPTY_LIST, null,
sortFields, sortField, pageSize, offset);
return list(query);
}
@@ -178,43 +192,88 @@ public class EPersonDAOImpl extends AbstractHibernateDSODAO<EPerson> implements
protected Query getSearchQuery(Context context, String queryString, String queryParam,
List<MetadataField> queryFields, List<MetadataField> sortFields, String sortField)
throws SQLException {
return getSearchQuery(context, queryString, queryParam, queryFields, sortFields, sortField, -1, -1);
return getSearchQuery(context, queryString, queryParam, queryFields, null, sortFields, sortField, -1, -1);
}
/**
* Build a search query across EPersons based on the given metadata fields and sorted based on the given metadata
* field(s) or database column.
* <P>
* NOTE: the EPerson's email address is included in the search alongside any given metadata fields.
*
* @param context DSpace Context
* @param queryString String which defines the beginning "SELECT" for the SQL query
* @param queryParam Actual text being searched for
* @param queryFields List of metadata fields to search within
* @param excludeGroup Optional Group which should be excluded from search. Any EPersons who are members
* of this group will not be included in the results.
* @param sortFields Optional List of metadata fields to sort by (should not be specified if sortField is used)
* @param sortField Optional database column to sort on (should not be specified if sortFields is used)
* @param pageSize how many results return
* @param offset the position of the first result to return
* @return built Query object
* @throws SQLException if error occurs
*/
protected Query getSearchQuery(Context context, String queryString, String queryParam,
List<MetadataField> queryFields, List<MetadataField> sortFields, String sortField,
List<MetadataField> queryFields, Group excludeGroup,
List<MetadataField> sortFields, String sortField,
int pageSize, int offset) throws SQLException {
// Initialize SQL statement using the passed in "queryString"
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append(queryString);
Set<MetadataField> metadataFieldsToJoin = new LinkedHashSet<>();
metadataFieldsToJoin.addAll(queryFields);
metadataFieldsToJoin.addAll(sortFields);
// Append necessary join information for MetadataFields we will search within
if (!CollectionUtils.isEmpty(metadataFieldsToJoin)) {
addMetadataLeftJoin(queryBuilder, EPerson.class.getSimpleName().toLowerCase(), metadataFieldsToJoin);
}
if (queryParam != null) {
// Always append a search on EPerson "email" based on query
if (StringUtils.isNotBlank(queryParam)) {
addMetadataValueWhereQuery(queryBuilder, queryFields, "like",
EPerson.class.getSimpleName().toLowerCase() + ".email like :queryParam");
}
// If excludeGroup is specified, exclude members of that group from results
// This uses a subquery to find the excluded group & verify that it is not in the EPerson list of "groups"
if (excludeGroup != null) {
// If query params exist, then we already have a WHERE clause (see above) and just need to append an AND
if (StringUtils.isNotBlank(queryParam)) {
queryBuilder.append(" AND ");
} else {
// no WHERE clause yet, so this is the start of the WHERE
queryBuilder.append(" WHERE ");
}
queryBuilder.append("(FROM Group g where g.id = :group_id) NOT IN elements (")
.append(EPerson.class.getSimpleName().toLowerCase()).append(".groups)");
}
// Add sort/order by info to query, if specified
if (!CollectionUtils.isEmpty(sortFields) || StringUtils.isNotBlank(sortField)) {
addMetadataSortQuery(queryBuilder, sortFields, Collections.singletonList(sortField));
}
// Create the final SQL SELECT statement (based on included params above)
Query query = createQuery(context, queryBuilder.toString());
// Set pagesize & offset for pagination
if (pageSize > 0) {
query.setMaxResults(pageSize);
}
if (offset > 0) {
query.setFirstResult(offset);
}
// Set all parameters to the SQL SELECT statement (based on included params above)
if (StringUtils.isNotBlank(queryParam)) {
query.setParameter("queryParam", "%" + queryParam.toLowerCase() + "%");
}
for (MetadataField metadataField : metadataFieldsToJoin) {
query.setParameter(metadataField.toString(), metadataField.getID());
}
if (excludeGroup != null) {
query.setParameter("group_id", excludeGroup.getID());
}
query.setHint("org.hibernate.cacheable", Boolean.TRUE);
return query;
}

View File

@@ -164,6 +164,41 @@ public class GroupDAOImpl extends AbstractHibernateDSODAO<Group> implements Grou
return count(query);
}
@Override
public List<Group> findByNameLikeAndNotMember(Context context, String groupName, Group excludeParent,
int offset, int limit) throws SQLException {
Query query = createQuery(context,
"FROM Group " +
"WHERE lower(name) LIKE lower(:group_name) " +
"AND id != :parent_id " +
"AND (from Group g where g.id = :parent_id) not in elements (parentGroups)");
query.setParameter("parent_id", excludeParent.getID());
query.setParameter("group_name", "%" + StringUtils.trimToEmpty(groupName) + "%");
if (0 <= offset) {
query.setFirstResult(offset);
}
if (0 <= limit) {
query.setMaxResults(limit);
}
query.setHint("org.hibernate.cacheable", Boolean.TRUE);
return list(query);
}
@Override
public int countByNameLikeAndNotMember(Context context, String groupName, Group excludeParent) throws SQLException {
Query query = createQuery(context,
"SELECT count(*) FROM Group " +
"WHERE lower(name) LIKE lower(:group_name) " +
"AND id != :parent_id " +
"AND (from Group g where g.id = :parent_id) not in elements (parentGroups)");
query.setParameter("parent_id", excludeParent.getID());
query.setParameter("group_name", "%" + StringUtils.trimToEmpty(groupName) + "%");
return count(query);
}
@Override
public void delete(Context context, Group group) throws SQLException {
Query query = getHibernateSession(context)
@@ -213,6 +248,7 @@ public class GroupDAOImpl extends AbstractHibernateDSODAO<Group> implements Grou
return list(query);
}
@Override
public int countByParent(Context context, Group parent) throws SQLException {
Query query = createQuery(context, "SELECT count(g) FROM Group g JOIN g.parentGroups pg " +
"WHERE pg.id = :parent_id");

View File

@@ -98,9 +98,9 @@ public interface EPersonService extends DSpaceObjectService<EPerson>, DSpaceObje
*
* @param context The relevant DSpace Context.
* @param query The search string
* @param offset Inclusive offset
* @param offset Inclusive offset (the position of the first result to return)
* @param limit Maximum number of matches returned
* @return array of EPerson objects
* @return List of matching EPerson objects
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
public List<EPerson> search(Context context, String query, int offset, int limit)
@@ -118,6 +118,34 @@ public interface EPersonService extends DSpaceObjectService<EPerson>, DSpaceObje
public int searchResultCount(Context context, String query)
throws SQLException;
/**
* Find the EPersons that match the search query which are NOT currently members of the given Group. The search
* query is run against firstname, lastname or email.
*
* @param context DSpace context
* @param query The search string
* @param excludeGroup Group to exclude results from. Members of this group will never be returned.
* @param offset Inclusive offset (the position of the first result to return)
* @param limit Maximum number of matches returned
* @return List of matching EPerson objects
* @throws SQLException if error
*/
List<EPerson> searchNonMembers(Context context, String query, Group excludeGroup,
int offset, int limit) throws SQLException;
/**
* Returns the total number of EPersons that match the search query which are NOT currently members of the given
* Group. The search query is run against firstname, lastname or email. Can be used with searchNonMembers() to
* support pagination
*
* @param context DSpace context
* @param query The search string
* @param excludeGroup Group to exclude results from. Members of this group will never be returned.
* @return List of matching EPerson objects
* @throws SQLException if error
*/
int searchNonMembersCount(Context context, String query, Group excludeGroup) throws SQLException;
/**
* Find all the {@code EPerson}s in a specific order by field.
* The sortable fields are:

View File

@@ -261,37 +261,67 @@ public interface GroupService extends DSpaceObjectService<Group>, DSpaceObjectLe
public List<Group> findAll(Context context, int sortField) throws SQLException;
/**
* Find the groups that match the search query across eperson_group_id or name
* Find the Groups that match the query across both Group name and Group ID. This is an unpaginated search,
* which means it will load all matching groups into memory at once. This may provide POOR PERFORMANCE when a large
* number of groups are matched.
*
* @param context DSpace context
* @param groupIdentifier The group name or group ID
* @return array of Group objects
* @param query The search string used to search across group name or group ID
* @return List of matching Group objects
* @throws SQLException if error
*/
public List<Group> search(Context context, String groupIdentifier) throws SQLException;
List<Group> search(Context context, String query) throws SQLException;
/**
* Find the groups that match the search query across eperson_group_id or name
* Find the Groups that match the query across both Group name and Group ID. This method supports pagination,
* which provides better performance than the above non-paginated search() method.
*
* @param context DSpace context
* @param groupIdentifier The group name or group ID
* @param offset Inclusive offset
* @param query The search string used to search across group name or group ID
* @param offset Inclusive offset (the position of the first result to return)
* @param limit Maximum number of matches returned
* @return array of Group objects
* @return List of matching Group objects
* @throws SQLException if error
*/
public List<Group> search(Context context, String groupIdentifier, int offset, int limit) throws SQLException;
List<Group> search(Context context, String query, int offset, int limit) throws SQLException;
/**
* Returns the total number of groups returned by a specific query, without the overhead
* of creating the Group objects to store the results.
* Returns the total number of Groups returned by a specific query. Search is performed based on Group name
* and Group ID. May be used with search() above to support pagination of matching Groups.
*
* @param context DSpace context
* @param query The search string
* @param query The search string used to search across group name or group ID
* @return the number of groups matching the query
* @throws SQLException if error
*/
public int searchResultCount(Context context, String query) throws SQLException;
int searchResultCount(Context context, String query) throws SQLException;
/**
* Find the groups that match the search query which are NOT currently members (subgroups)
* of the given parentGroup
*
* @param context DSpace context
* @param query The search string used to search across group name or group ID
* @param excludeParentGroup Parent group to exclude results from
* @param offset Inclusive offset (the position of the first result to return)
* @param limit Maximum number of matches returned
* @return List of matching Group objects
* @throws SQLException if error
*/
List<Group> searchNonMembers(Context context, String query, Group excludeParentGroup,
int offset, int limit) throws SQLException;
/**
* Returns the total number of groups that match the search query which are NOT currently members (subgroups)
* of the given parentGroup. Can be used with searchNonMembers() to support pagination.
*
* @param context DSpace context
* @param query The search string used to search across group name or group ID
* @param excludeParentGroup Parent group to exclude results from
* @return the number of Groups matching the query
* @throws SQLException if error
*/
int searchNonMembersCount(Context context, String query, Group excludeParentGroup) throws SQLException;
/**
* Return true if group has no direct or indirect members

View File

@@ -8,6 +8,7 @@
package org.dspace.eperson;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -15,6 +16,8 @@ import static org.junit.Assert.fail;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@@ -277,63 +280,184 @@ public class EPersonTest extends AbstractUnitTest {
*/
/**
* Test of search method, of class EPerson.
* Test of search() and searchResultCount() methods of EPersonService
* NOTE: Pagination is not verified here because it is tested in EPersonRestRepositoryIT
*/
/*
@Test
public void testSearch_Context_String()
throws Exception
{
System.out.println("search");
Context context = null;
String query = "";
EPerson[] expResult = null;
EPerson[] result = EPerson.search(context, query);
assertEquals(expResult, result);
// TODO review the generated test code and remove the default call to fail.
fail("The test case is a prototype.");
public void testSearchAndCountByNameEmail() throws SQLException, AuthorizeException, IOException {
List<EPerson> allEPeopleAdded = new ArrayList<>();
Group testGroup = createGroup("TestingGroup");
try {
// Create 4 EPersons. Add a few to a test group to verify group membership doesn't matter
EPerson eperson1 = createEPersonAndAddToGroup("eperson1@example.com", "Jane", "Doe", testGroup);
EPerson eperson2 = createEPerson("eperson2@example.com", "John", "Doe");
EPerson eperson3 = createEPersonAndAddToGroup("eperson3@example.com", "John", "Smith", testGroup);
EPerson eperson4 = createEPerson("eperson4@example.com", "Doe", "Smith");
allEPeopleAdded.addAll(Arrays.asList(eperson1, eperson2, eperson3, eperson4));
List<EPerson> allJohns = Arrays.asList(eperson2, eperson3);
List<EPerson> searchJohnResults = ePersonService.search(context, "John", -1, -1);
assertTrue(searchJohnResults.containsAll(allJohns));
assertEquals(searchJohnResults.size(), ePersonService.searchResultCount(context, "John"));
List<EPerson> allDoes = Arrays.asList(eperson1, eperson2, eperson4);
List<EPerson> searchDoeResults = ePersonService.search(context, "Doe", -1, -1);
assertTrue(searchDoeResults.containsAll(allDoes));
assertEquals(searchDoeResults.size(), ePersonService.searchResultCount(context, "Doe"));
List<EPerson> allSmiths = Arrays.asList(eperson3, eperson4);
List<EPerson> searchSmithResults = ePersonService.search(context, "Smith", -1, -1);
assertTrue(searchSmithResults.containsAll(allSmiths));
assertEquals(searchSmithResults.size(), ePersonService.searchResultCount(context, "Smith"));
// Assert search on example.com returns everyone
List<EPerson> searchEmailResults = ePersonService.search(context, "example.com", -1, -1);
assertTrue(searchEmailResults.containsAll(allEPeopleAdded));
assertEquals(searchEmailResults.size(), ePersonService.searchResultCount(context, "example.com"));
// Assert exact email search returns just one
List<EPerson> exactEmailResults = ePersonService.search(context, "eperson1@example.com", -1, -1);
assertTrue(exactEmailResults.contains(eperson1));
assertEquals(exactEmailResults.size(), ePersonService.searchResultCount(context, "eperson1@example.com"));
// Assert UUID search returns exact match
List<EPerson> uuidResults = ePersonService.search(context, eperson4.getID().toString(), -1, -1);
assertTrue(uuidResults.contains(eperson4));
assertEquals(1, uuidResults.size());
assertEquals(uuidResults.size(), ePersonService.searchResultCount(context, eperson4.getID().toString()));
} finally {
// Remove all Groups & EPersons we added for this test
context.turnOffAuthorisationSystem();
groupService.delete(context, testGroup);
for (EPerson ePerson : allEPeopleAdded) {
ePersonService.delete(context, ePerson);
}
context.restoreAuthSystemState();
}
}
*/
/**
* Test of search method, of class EPerson.
* Test of searchNonMembers() and searchNonMembersCount() methods of EPersonService
* NOTE: Pagination is not verified here because it is tested in EPersonRestRepositoryIT
*/
/*
@Test
public void testSearch_4args()
throws Exception
{
System.out.println("search");
Context context = null;
String query = "";
int offset = 0;
int limit = 0;
EPerson[] expResult = null;
EPerson[] result = EPerson.search(context, query, offset, limit);
assertEquals(expResult, result);
// TODO review the generated test code and remove the default call to fail.
fail("The test case is a prototype.");
}
*/
public void testSearchAndCountByNameEmailNonMembers() throws SQLException, AuthorizeException, IOException {
List<EPerson> allEPeopleAdded = new ArrayList<>();
Group testGroup1 = createGroup("TestingGroup1");
Group testGroup2 = createGroup("TestingGroup2");
Group testGroup3 = createGroup("TestingGroup3");
try {
// Create two EPersons in Group 1
EPerson eperson1 = createEPersonAndAddToGroup("eperson1@example.com", "Jane", "Doe", testGroup1);
EPerson eperson2 = createEPersonAndAddToGroup("eperson2@example.com", "John", "Smith", testGroup1);
/**
* Test of searchResultCount method, of class EPerson.
*/
/*
@Test
public void testSearchResultCount()
throws Exception
{
System.out.println("searchResultCount");
Context context = null;
String query = "";
int expResult = 0;
int result = EPerson.searchResultCount(context, query);
assertEquals(expResult, result);
// TODO review the generated test code and remove the default call to fail.
fail("The test case is a prototype.");
// Create one more EPerson, and add it and a previous EPerson to Group 2
EPerson eperson3 = createEPersonAndAddToGroup("eperson3@example.com", "John", "Doe", testGroup2);
context.turnOffAuthorisationSystem();
groupService.addMember(context, testGroup2, eperson2);
groupService.update(context, testGroup2);
ePersonService.update(context, eperson2);
context.restoreAuthSystemState();
// Create 2 more EPersons with no group memberships
EPerson eperson4 = createEPerson("eperson4@example.com", "John", "Anthony");
EPerson eperson5 = createEPerson("eperson5@example.org", "Smith", "Doe");
allEPeopleAdded.addAll(Arrays.asList(eperson1, eperson2, eperson3, eperson4, eperson5));
// FIRST, test search by last name
// Verify all Does match a nonMember search of Group3 (which is an empty group)
List<EPerson> allDoes = Arrays.asList(eperson1, eperson3, eperson5);
List<EPerson> searchDoeResults = ePersonService.searchNonMembers(context, "Doe", testGroup3, -1, -1);
assertTrue(searchDoeResults.containsAll(allDoes));
assertEquals(searchDoeResults.size(), ePersonService.searchNonMembersCount(context, "Doe", testGroup3));
// Verify searching "Doe" with Group 2 *excludes* the one which is already a member
List<EPerson> allNonMemberDoes = Arrays.asList(eperson1, eperson5);
List<EPerson> searchNonMemberDoeResults = ePersonService.searchNonMembers(context, "Doe", testGroup2,
-1, -1);
assertTrue(searchNonMemberDoeResults.containsAll(allNonMemberDoes));
assertFalse(searchNonMemberDoeResults.contains(eperson3));
assertEquals(searchNonMemberDoeResults.size(), ePersonService.searchNonMembersCount(context, "Doe",
testGroup2));
// Verify searching "Doe" with Group 1 *excludes* the one which is already a member
allNonMemberDoes = Arrays.asList(eperson3, eperson5);
searchNonMemberDoeResults = ePersonService.searchNonMembers(context, "Doe", testGroup1, -1, -1);
assertTrue(searchNonMemberDoeResults.containsAll(allNonMemberDoes));
assertFalse(searchNonMemberDoeResults.contains(eperson1));
assertEquals(searchNonMemberDoeResults.size(), ePersonService.searchNonMembersCount(context, "Doe",
testGroup1));
// SECOND, test search by first name
// Verify all Johns match a nonMember search of Group3 (which is an empty group)
List<EPerson> allJohns = Arrays.asList(eperson2, eperson3, eperson4);
List<EPerson> searchJohnResults = ePersonService.searchNonMembers(context, "John",
testGroup3, -1, -1);
assertTrue(searchJohnResults.containsAll(allJohns));
assertEquals(searchJohnResults.size(), ePersonService.searchNonMembersCount(context, "John",
testGroup3));
// Verify searching "John" with Group 2 *excludes* the two who are already a member
List<EPerson> allNonMemberJohns = Arrays.asList(eperson4);
List<EPerson> searchNonMemberJohnResults = ePersonService.searchNonMembers(context, "John",
testGroup2, -1, -1);
assertTrue(searchNonMemberJohnResults.containsAll(allNonMemberJohns));
assertFalse(searchNonMemberJohnResults.contains(eperson2));
assertFalse(searchNonMemberJohnResults.contains(eperson3));
assertEquals(searchNonMemberJohnResults.size(), ePersonService.searchNonMembersCount(context, "John",
testGroup2));
// FINALLY, test search by email
// Assert search on example.com excluding Group 1 returns just those not in that group
List<EPerson> exampleNonMembers = Arrays.asList(eperson3, eperson4);
List<EPerson> searchEmailResults = ePersonService.searchNonMembers(context, "example.com",
testGroup1, -1, -1);
assertTrue(searchEmailResults.containsAll(exampleNonMembers));
assertFalse(searchEmailResults.contains(eperson1));
assertFalse(searchEmailResults.contains(eperson2));
assertEquals(searchEmailResults.size(), ePersonService.searchNonMembersCount(context, "example.com",
testGroup1));
// Assert exact email search returns just one (if not in group)
List<EPerson> exactEmailResults = ePersonService.searchNonMembers(context, "eperson1@example.com",
testGroup2, -1, -1);
assertTrue(exactEmailResults.contains(eperson1));
assertEquals(exactEmailResults.size(), ePersonService.searchNonMembersCount(context, "eperson1@example.com",
testGroup2));
// But, change the group to one they are a member of, and they won't be included
exactEmailResults = ePersonService.searchNonMembers(context, "eperson1@example.com",
testGroup1, -1, -1);
assertFalse(exactEmailResults.contains(eperson1));
assertEquals(exactEmailResults.size(), ePersonService.searchNonMembersCount(context, "eperson1@example.com",
testGroup1));
// Assert UUID search returns exact match (if not in group)
List<EPerson> uuidResults = ePersonService.searchNonMembers(context, eperson3.getID().toString(),
testGroup1, -1, -1);
assertTrue(uuidResults.contains(eperson3));
assertEquals(1, uuidResults.size());
assertEquals(uuidResults.size(), ePersonService.searchNonMembersCount(context, eperson3.getID().toString(),
testGroup1));
// But, change the group to one they are a member of, and you'll get no results
uuidResults = ePersonService.searchNonMembers(context, eperson3.getID().toString(),
testGroup2, -1, -1);
assertFalse(uuidResults.contains(eperson3));
assertEquals(0, uuidResults.size());
assertEquals(uuidResults.size(), ePersonService.searchNonMembersCount(context, eperson3.getID().toString(),
testGroup2));
} finally {
// Remove all Groups & EPersons we added for this test
context.turnOffAuthorisationSystem();
groupService.delete(context, testGroup1);
groupService.delete(context, testGroup2);
groupService.delete(context, testGroup3);
for (EPerson ePerson : allEPeopleAdded) {
ePersonService.delete(context, ePerson);
}
context.restoreAuthSystemState();
}
}
*/
/**
* Test of findAll method, of class EPerson.
@@ -1149,6 +1273,17 @@ public class EPersonTest extends AbstractUnitTest {
return ePerson;
}
protected EPerson createEPersonAndAddToGroup(String email, String firstname, String lastname, Group group)
throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();
EPerson ePerson = createEPerson(email, firstname, lastname);
groupService.addMember(context, group, ePerson);
groupService.update(context, group);
ePersonService.update(context, ePerson);
context.restoreAuthSystemState();
return ePerson;
}
protected EPerson createEPerson(String email) throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();
EPerson ePerson = ePersonService.create(context);
@@ -1157,4 +1292,15 @@ public class EPersonTest extends AbstractUnitTest {
context.restoreAuthSystemState();
return ePerson;
}
protected EPerson createEPerson(String email, String firstname, String lastname)
throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();
EPerson ePerson = ePersonService.create(context);
ePerson.setEmail(email);
ePerson.setFirstName(context, firstname);
ePerson.setLastName(context, lastname);
ePersonService.update(context, ePerson);
context.restoreAuthSystemState();
return ePerson;
}
}

View File

@@ -680,6 +680,109 @@ public class GroupTest extends AbstractUnitTest {
}
}
@Test
// Tests searchNonMembers() and searchNonMembersCount()
// NOTE: This does not test pagination as that is tested in GroupRestRepositoryIT in server-webapp
public void searchAndCountNonMembers() throws SQLException, AuthorizeException, IOException {
// Create a parent group with 2 child groups
Group parentGroup = createGroup("Some Parent Group");
Group someStaffGroup = createGroup("Some Other Staff");
Group someStudentsGroup = createGroup("Some Students");
groupService.addMember(context, parentGroup, someStaffGroup);
groupService.addMember(context, parentGroup, someStudentsGroup);
groupService.update(context, parentGroup);
// Create a separate parent which is not a member of the first & add two child groups to it
Group studentsNotInParentGroup = createGroup("Students not in Parent");
Group otherStudentsNotInParentGroup = createGroup("Other Students");
Group someOtherStudentsNotInParentGroup = createGroup("Some Other Students");
groupService.addMember(context, studentsNotInParentGroup, otherStudentsNotInParentGroup);
groupService.addMember(context, studentsNotInParentGroup, someOtherStudentsNotInParentGroup);
groupService.update(context, studentsNotInParentGroup);
try {
// Assert that all Groups *not* in parent group match an empty search
List<Group> notInParent = Arrays.asList(studentsNotInParentGroup, otherStudentsNotInParentGroup,
someOtherStudentsNotInParentGroup);
List<Group> nonMembersSearch = groupService.searchNonMembers(context, "", parentGroup, -1, -1);
// NOTE: Because others unit tests create groups, this search will return an undetermined number of results.
// Therefore, we just verify that our expected groups are included and others are NOT included.
assertTrue(nonMembersSearch.containsAll(notInParent));
// Verify it does NOT contain members of parentGroup
assertFalse(nonMembersSearch.contains(someStaffGroup));
assertFalse(nonMembersSearch.contains(someStudentsGroup));
// Verify it also does NOT contain the parentGroup itself
assertFalse(nonMembersSearch.contains(parentGroup));
// Verify the count for empty search matches the size of the search results
assertEquals(nonMembersSearch.size(), groupService.searchNonMembersCount(context, "", parentGroup));
// Assert a search on "Students" matches all those same groups (as they all include that word in their name)
nonMembersSearch = groupService.searchNonMembers(context, "Students", parentGroup, -1, -1);
assertTrue(nonMembersSearch.containsAll(notInParent));
//Verify an existing member group with "Students" in its name does NOT get returned
assertFalse(nonMembersSearch.contains(someStudentsGroup));
assertEquals(nonMembersSearch.size(),
groupService.searchNonMembersCount(context, "Students", parentGroup));
// Assert a search on "other" matches just two groups
// (this also tests search is case insensitive)
nonMembersSearch = groupService.searchNonMembers(context, "other", parentGroup, -1, -1);
assertTrue(nonMembersSearch.containsAll(
Arrays.asList(otherStudentsNotInParentGroup, someOtherStudentsNotInParentGroup)));
// Verify an existing member group with "Other" in its name does NOT get returned
assertFalse(nonMembersSearch.contains(someStaffGroup));
assertEquals(nonMembersSearch.size(), groupService.searchNonMembersCount(context, "other", parentGroup));
// Assert a search on "Parent" matches just one group
nonMembersSearch = groupService.searchNonMembers(context, "Parent", parentGroup, -1, -1);
assertTrue(nonMembersSearch.contains(studentsNotInParentGroup));
// Verify Parent Group itself does NOT get returned
assertFalse(nonMembersSearch.contains(parentGroup));
assertEquals(nonMembersSearch.size(), groupService.searchNonMembersCount(context, "Parent", parentGroup));
// Assert a UUID search matching a non-member group will return just that one group
nonMembersSearch = groupService.searchNonMembers(context,
someOtherStudentsNotInParentGroup.getID().toString(),
parentGroup, -1, -1);
assertEquals(1, nonMembersSearch.size());
assertTrue(nonMembersSearch.contains(someOtherStudentsNotInParentGroup));
assertEquals(nonMembersSearch.size(),
groupService.searchNonMembersCount(context,
someOtherStudentsNotInParentGroup.getID().toString(),
parentGroup));
// Assert a UUID search matching an EXISTING member will return NOTHING
// (as this group is excluded from the search)
nonMembersSearch = groupService.searchNonMembers(context, someStudentsGroup.getID().toString(),
parentGroup,-1, -1);
assertEquals(0, nonMembersSearch.size());
assertEquals(nonMembersSearch.size(),
groupService.searchNonMembersCount(context, someStudentsGroup.getID().toString(),
parentGroup));
// Assert a UUID search matching Parent Group *itself* will return NOTHING
// (as this group is excluded from the search)
nonMembersSearch = groupService.searchNonMembers(context, parentGroup.getID().toString(),
parentGroup,-1, -1);
assertEquals(0, nonMembersSearch.size());
assertEquals(nonMembersSearch.size(),
groupService.searchNonMembersCount(context, parentGroup.getID().toString(),
parentGroup));
} finally {
// Clean up our data
context.turnOffAuthorisationSystem();
groupService.delete(context, parentGroup);
groupService.delete(context, someStaffGroup);
groupService.delete(context, someStudentsGroup);
groupService.delete(context, studentsNotInParentGroup);
groupService.delete(context, otherStudentsNotInParentGroup);
groupService.delete(context, someOtherStudentsNotInParentGroup);
context.restoreAuthSystemState();
}
}
protected Group createGroup(String name) throws SQLException, AuthorizeException {
context.turnOffAuthorisationSystem();

View File

@@ -38,9 +38,11 @@ import org.dspace.authorize.service.ValidatePasswordService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.EmptyWorkflowGroupException;
import org.dspace.eperson.Group;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
@@ -79,6 +81,9 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
@Autowired
private RegistrationDataService registrationDataService;
@Autowired
private GroupService groupService;
private final EPersonService es;
@@ -289,6 +294,35 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository<EPerson, E
}
}
/**
* Find the EPersons matching the query parameter which are NOT a member of the given Group.
* The search is delegated to the
* {@link EPersonService#searchNonMembers(Context, String, Group, int, int)} method
*
* @param groupUUID the *required* group UUID to exclude results from
* @param query is the *required* query string
* @param pageable contains the pagination information
* @return a Page of EPersonRest instances matching the user query
*/
@PreAuthorize("hasAuthority('ADMIN') || hasAuthority('MANAGE_ACCESS_GROUP')")
@SearchRestMethod(name = "isNotMemberOf")
public Page<EPersonRest> findIsNotMemberOf(@Parameter(value = "group", required = true) UUID groupUUID,
@Parameter(value = "query", required = true) String query,
Pageable pageable) {
try {
Context context = obtainContext();
Group excludeGroup = groupService.find(context, groupUUID);
long total = es.searchNonMembersCount(context, query, excludeGroup);
List<EPerson> epersons = es.searchNonMembers(context, query, excludeGroup,
Math.toIntExact(pageable.getOffset()),
Math.toIntExact(pageable.getPageSize()));
return converter.toRestPage(epersons, pageable, total, utils.obtainProjection());
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
@PreAuthorize("hasPermission(#uuid, 'EPERSON', #patch)")
protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID uuid,

View File

@@ -148,6 +148,35 @@ public class GroupRestRepository extends DSpaceObjectRestRepository<Group, Group
}
}
/**
* Find the Groups matching the query parameter which are NOT a member of the given parent Group.
* The search is delegated to the
* {@link GroupService#searchNonMembers(Context, String, Group, int, int)} method
*
* @param groupUUID the parent group UUID
* @param query is the *required* query string
* @param pageable contains the pagination information
* @return a Page of GroupRest instances matching the user query
*/
@PreAuthorize("hasAuthority('ADMIN') || hasAuthority('MANAGE_ACCESS_GROUP')")
@SearchRestMethod(name = "isNotMemberOf")
public Page<GroupRest> findIsNotMemberOf(@Parameter(value = "group", required = true) UUID groupUUID,
@Parameter(value = "query", required = true) String query,
Pageable pageable) {
try {
Context context = obtainContext();
Group excludeParentGroup = gs.find(context, groupUUID);
long total = gs.searchNonMembersCount(context, query, excludeParentGroup);
List<Group> groups = gs.searchNonMembers(context, query, excludeParentGroup,
Math.toIntExact(pageable.getOffset()),
Math.toIntExact(pageable.getPageSize()));
return converter.toRestPage(groups, pageable, total, utils.obtainProjection());
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public Class<GroupRest> getDomainClass() {
return GroupRest.class;

View File

@@ -79,6 +79,7 @@ import org.dspace.eperson.Group;
import org.dspace.eperson.PasswordHash;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matchers;
@@ -96,6 +97,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
@Autowired
private EPersonService ePersonService;
@Autowired
private GroupService groupService;
@Autowired
private ConfigurationService configurationService;
@@ -775,6 +779,242 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(status().isBadRequest());
}
// Test of /epersons/search/isNotMemberOf pagination
// NOTE: Additional tests of 'isNotMemberOf' search functionality can be found in EPersonTest in 'dspace-api'
@Test
public void searchIsNotMemberOfPaginationTest() throws Exception {
context.turnOffAuthorisationSystem();
Group group = GroupBuilder.createGroup(context)
.withName("Test Parent group")
.build();
// Create two EPerson in main group. These SHOULD NOT be included in pagination
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test", "Person")
.withEmail("test@example.com")
.withGroupMembership(group)
.build();
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test2", "Person")
.withEmail("test2@example.com")
.withGroupMembership(group)
.build();
// Create five EPersons who are NOT members of that group. These SHOULD be included in pagination
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test3", "Person")
.withEmail("test3@example.com")
.build();
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test4", "Person")
.withEmail("test4@example.com")
.build();
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test5", "Person")
.withEmail("test5@example.com")
.build();
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test6", "Person")
.withEmail("test6@example.com")
.build();
EPersonBuilder.createEPerson(context)
.withNameInMetadata("Test7", "Person")
.withEmail("test7@example.com")
.build();
context.restoreAuthSystemState();
String authTokenAdmin = getAuthToken(admin.getEmail(), password);
getClient(authTokenAdmin).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "person")
.param("page", "0")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.everyItem(
hasJsonPath("$.type", is("eperson")))
))
.andExpect(jsonPath("$._embedded.epersons").value(Matchers.hasSize(2)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(0)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
getClient(authTokenAdmin).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "person")
.param("page", "1")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.everyItem(
hasJsonPath("$.type", is("eperson")))
))
.andExpect(jsonPath("$._embedded.epersons").value(Matchers.hasSize(2)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(1)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
getClient(authTokenAdmin).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "person")
.param("page", "2")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.everyItem(
hasJsonPath("$.type", is("eperson")))
))
.andExpect(jsonPath("$._embedded.epersons").value(Matchers.hasSize(1)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(2)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
}
@Test
public void searchIsNotMemberOfByEmail() throws Exception {
context.turnOffAuthorisationSystem();
Group group = GroupBuilder.createGroup(context)
.withName("Test group")
.build();
Group group2 = GroupBuilder.createGroup(context)
.withName("Test another group")
.build();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withNameInMetadata("John", "Doe")
.withEmail("Johndoe@example.com")
.withGroupMembership(group)
.build();
EPerson ePerson2 = EPersonBuilder.createEPerson(context)
.withNameInMetadata("Jane", "Smith")
.withEmail("janesmith@example.com")
.build();
EPerson ePerson3 = EPersonBuilder.createEPerson(context)
.withNameInMetadata("Tom", "Doe")
.withEmail("tomdoe@example.com")
.build();
EPerson ePerson4 = EPersonBuilder.createEPerson(context)
.withNameInMetadata("Harry", "Prefix-Doe")
.withEmail("harrydoeprefix@example.com")
.build();
context.restoreAuthSystemState();
String authToken = getAuthToken(admin.getEmail(), password);
// Search for exact email in a group the person already belongs to. Should return zero results.
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", ePerson.getEmail())
.param("group", group.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.page.totalElements", is(0)));
// Search for exact email in a group the person does NOT belong to. Should return the person
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", ePerson.getEmail())
.param("group", group2.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.contains(
EPersonMatcher.matchEPersonEntry(ePerson)
)))
.andExpect(jsonPath("$.page.totalElements", is(1)));
// Search partial email should return all the people created above.
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", "example.com")
.param("group", group2.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.containsInAnyOrder(
EPersonMatcher.matchEPersonEntry(ePerson),
EPersonMatcher.matchEPersonEntry(ePerson2),
EPersonMatcher.matchEPersonEntry(ePerson3),
EPersonMatcher.matchEPersonEntry(ePerson4)
)));
}
@Test
public void searchIsNotMemberOfByUUID() throws Exception {
context.turnOffAuthorisationSystem();
Group group = GroupBuilder.createGroup(context)
.withName("Test group")
.build();
Group group2 = GroupBuilder.createGroup(context)
.withName("Test another group")
.build();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withNameInMetadata("John", "Doe")
.withEmail("Johndoe@example.com")
.withGroupMembership(group)
.build();
context.restoreAuthSystemState();
String authToken = getAuthToken(admin.getEmail(), password);
// Search for UUID in a group the person already belongs to. Should return zero results.
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", ePerson.getID().toString())
.param("group", group.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.page.totalElements", is(0)));
// Search for exact email in a group the person does NOT belong to. Should return the person
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", ePerson.getID().toString())
.param("group", group2.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.epersons", Matchers.contains(
EPersonMatcher.matchEPersonEntry(ePerson)
)))
.andExpect(jsonPath("$.page.totalElements", is(1)));
}
@Test
public void searchIsNotMemberOfUnauthorized() throws Exception {
Group adminGroup = groupService.findByName(context, Group.ADMIN);
getClient().perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", eperson.getID().toString())
.param("group", adminGroup.getID().toString()))
.andExpect(status().isUnauthorized());
}
@Test
public void searchIsNotMemberOfForbidden() throws Exception {
Group adminGroup = groupService.findByName(context, Group.ADMIN);
String authToken = getAuthToken(eperson.getEmail(), password);
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", eperson.getID().toString())
.param("group", adminGroup.getID().toString()))
.andExpect(status().isForbidden());
}
@Test
public void searchIsNotMemberOfMissingOrInvalidParameter() throws Exception {
Group adminGroup = groupService.findByName(context, Group.ADMIN);
String authToken = getAuthToken(admin.getEmail(), password);
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf"))
.andExpect(status().isBadRequest());
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", eperson.getID().toString()))
.andExpect(status().isBadRequest());
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("group", adminGroup.getID().toString()))
.andExpect(status().isBadRequest());
// Test invalid group UUID
getClient(authToken).perform(get("/api/eperson/epersons/search/isNotMemberOf")
.param("query", eperson.getID().toString())
.param("group", "not-a-uuid"))
.andExpect(status().isBadRequest());
}
@Test
public void deleteOne() throws Exception {
context.turnOffAuthorisationSystem();

View File

@@ -3242,6 +3242,192 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$.page.totalElements", is(5)));
}
// Test of /groups/search/isNotMemberOf pagination
// NOTE: Additional tests of 'isNotMemberOf' search functionality can be found in GroupTest in 'dspace-api'
@Test
public void searchIsNotMemberOfPaginationTest() throws Exception {
context.turnOffAuthorisationSystem();
Group group = GroupBuilder.createGroup(context)
.withName("Test Parent group")
.build();
// Create two subgroups of main group. These SHOULD NOT be included in pagination
GroupBuilder.createGroup(context)
.withParent(group)
.withName("Test group 1")
.build();
GroupBuilder.createGroup(context)
.withParent(group)
.withName("Test group 2")
.build();
// Create five non-member groups. These SHOULD be included in pagination
GroupBuilder.createGroup(context)
.withName("Test group 3")
.build();
GroupBuilder.createGroup(context)
.withName("Test group 4")
.build();
GroupBuilder.createGroup(context)
.withName("Test group 5")
.build();
GroupBuilder.createGroup(context)
.withName("Test group 6")
.build();
GroupBuilder.createGroup(context)
.withName("Test group 7")
.build();
context.restoreAuthSystemState();
String authTokenAdmin = getAuthToken(admin.getEmail(), password);
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "test group")
.param("page", "0")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.groups", Matchers.everyItem(
hasJsonPath("$.type", is("group")))
))
.andExpect(jsonPath("$._embedded.groups").value(Matchers.hasSize(2)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(0)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "test group")
.param("page", "1")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.groups", Matchers.everyItem(
hasJsonPath("$.type", is("group")))
))
.andExpect(jsonPath("$._embedded.groups").value(Matchers.hasSize(2)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(1)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group.getID().toString())
.param("query", "test group")
.param("page", "2")
.param("size", "2"))
.andExpect(status().isOk()).andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.groups", Matchers.everyItem(
hasJsonPath("$.type", is("group")))
))
.andExpect(jsonPath("$._embedded.groups").value(Matchers.hasSize(1)))
.andExpect(jsonPath("$.page.size", is(2)))
.andExpect(jsonPath("$.page.number", is(2)))
.andExpect(jsonPath("$.page.totalPages", is(3)))
.andExpect(jsonPath("$.page.totalElements", is(5)));
}
@Test
public void searchIsNotMemberOfByUUID() throws Exception {
context.turnOffAuthorisationSystem();
// Create two groups which have no parent group
Group group1 = GroupBuilder.createGroup(context)
.withName("Test Parent group 1")
.build();
Group group2 = GroupBuilder.createGroup(context)
.withName("Test Parent group 2")
.build();
// Create a subgroup of parent group 1
Group group3 = GroupBuilder.createGroup(context)
.withParent(group1)
.withName("Test subgroup")
.build();
context.restoreAuthSystemState();
String authTokenAdmin = getAuthToken(admin.getEmail(), password);
// Search for UUID in a group that the subgroup already belongs to. Should return ZERO results.
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group1.getID().toString())
.param("query", group3.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.page.totalElements", is(0)));
// Search for UUID in a group that the subgroup does NOT belong to. Should return group via exact match
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group2.getID().toString())
.param("query", group3.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$._embedded.groups", Matchers.contains(
GroupMatcher.matchGroupEntry(group3.getID(), group3.getName())
)))
.andExpect(jsonPath("$.page.totalElements", is(1)));
// Search for UUID of the group in the "group" param. Should return ZERO results, as "group" param is excluded
getClient(authTokenAdmin).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", group1.getID().toString())
.param("query", group1.getID().toString()))
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.page.totalElements", is(0)));
}
@Test
public void searchIsNotMemberOfUnauthorized() throws Exception {
// To avoid creating data, just use the Admin & Anon groups for this test
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
Group adminGroup = groupService.findByName(context, Group.ADMIN);
Group anonGroup = groupService.findByName(context, Group.ANONYMOUS);
getClient().perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("query", anonGroup.getID().toString())
.param("group", adminGroup.getID().toString()))
.andExpect(status().isUnauthorized());
}
@Test
public void searchIsNotMemberOfForbidden() throws Exception {
// To avoid creating data, just use the Admin & Anon groups for this test
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
Group adminGroup = groupService.findByName(context, Group.ADMIN);
Group anonGroup = groupService.findByName(context, Group.ANONYMOUS);
String authToken = getAuthToken(eperson.getEmail(), password);
getClient(authToken).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("query", anonGroup.getID().toString())
.param("group", adminGroup.getID().toString()))
.andExpect(status().isForbidden());
}
@Test
public void searchIsNotMemberOfMissingOrInvalidParameter() throws Exception {
// To avoid creating data, just use the Admin & Anon groups for this test
GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
Group adminGroup = groupService.findByName(context, Group.ADMIN);
Group anonGroup = groupService.findByName(context, Group.ANONYMOUS);
String authToken = getAuthToken(admin.getEmail(), password);
getClient(authToken).perform(get("/api/eperson/groups/search/isNotMemberOf"))
.andExpect(status().isBadRequest());
getClient(authToken).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("query", anonGroup.getID().toString()))
.andExpect(status().isBadRequest());
getClient(authToken).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("group", adminGroup.getID().toString()))
.andExpect(status().isBadRequest());
// Test invalid group UUID
getClient(authToken).perform(get("/api/eperson/groups/search/isNotMemberOf")
.param("query", anonGroup.getID().toString())
.param("group", "not-a-uuid"))
.andExpect(status().isBadRequest());
}
@Test
public void commAdminAndColAdminCannotExploitItemReadGroupTest() throws Exception {