/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE and NOTICE files at the root of the source * tree and available online at * * http://www.dspace.org/license/ */ package org.dspace.eperson; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.dspace.authorize.AuthorizeConfiguration; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObjectServiceImpl; import org.dspace.content.MetadataField; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogManager; import org.dspace.eperson.dao.Group2GroupCacheDAO; import org.dspace.eperson.dao.GroupDAO; import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.dspace.event.Event; import org.dspace.util.UUIDUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.sql.SQLException; import java.util.*; /** * Service implementation for the Group object. * This class is responsible for all business logic calls for the Group object and is autowired by spring. * This class should never be accessed directly. * * @author kevinvandevelde at atmire.com */ public class GroupServiceImpl extends DSpaceObjectServiceImpl<Group> implements GroupService { private static final Logger log = LoggerFactory.getLogger(GroupServiceImpl.class); @Autowired(required = true) protected GroupDAO groupDAO; @Autowired(required = true) protected Group2GroupCacheDAO group2GroupCacheDAO; @Autowired(required = true) protected CollectionService collectionService; @Autowired(required = true) protected EPersonService ePersonService; @Autowired(required = true) protected CommunityService communityService; @Autowired(required = true) protected AuthorizeService authorizeService; protected GroupServiceImpl() { super(); } @Override public Group create(Context context) throws SQLException, AuthorizeException { // FIXME - authorization? if (!authorizeService.isAdmin(context)) { throw new AuthorizeException( "You must be an admin to create an EPerson Group"); } // Create a table row Group g = groupDAO.create(context, new Group()); log.info(LogManager.getHeader(context, "create_group", "group_id=" + g.getID())); context.addEvent(new Event(Event.CREATE, Constants.GROUP, g.getID(), null, getIdentifiers(context, g))); update(context, g); return g; } @Override public void setName(Group group, String name) throws SQLException { if (group.isPermanent()) { log.error("Attempt to rename permanent Group {} to {}.", group.getName(), name); throw new SQLException("Attempt to rename a permanent Group"); } else group.setName(name); } @Override public void addMember(Context context, Group group, EPerson e) { if (isDirectMember(group, e)) { return; } group.addMember(e); e.getGroups().add(group); context.addEvent(new Event(Event.ADD, Constants.GROUP, group.getID(), Constants.EPERSON, e.getID(), e.getEmail(), getIdentifiers(context, group))); } @Override public void addMember(Context context, Group groupParent, Group groupChild) throws SQLException { // don't add if it's already a member // and don't add itself if (groupParent.contains(groupChild) || groupParent.getID()==groupChild.getID()) { return; } groupParent.addMember(groupChild); groupChild.addParentGroup(groupParent); context.addEvent(new Event(Event.ADD, Constants.GROUP, groupParent.getID(), Constants.GROUP, groupChild.getID(), groupChild.getName(), getIdentifiers(context, groupParent))); } @Override public void removeMember(Context context, Group group, EPerson ePerson) { if (group.remove(ePerson)) { context.addEvent(new Event(Event.REMOVE, Constants.GROUP, group.getID(), Constants.EPERSON, ePerson.getID(), ePerson.getEmail(), getIdentifiers(context, group))); } } @Override public void removeMember(Context context, Group groupParent, Group childGroup) throws SQLException { if (groupParent.remove(childGroup)) { childGroup.removeParentGroup(groupParent); context.addEvent(new Event(Event.REMOVE, Constants.GROUP, groupParent.getID(), Constants.GROUP, childGroup.getID(), childGroup.getName(), getIdentifiers(context, groupParent))); } } @Override public boolean isDirectMember(Group group, EPerson ePerson) { // special, group 0 is anonymous return StringUtils.equals(group.getName(), Group.ANONYMOUS) || group.contains(ePerson); } @Override public boolean isMember(Group owningGroup, Group childGroup) { return owningGroup.contains(childGroup); } @Override public boolean isMember(Context context, Group group) throws SQLException { return isMember(context, group.getName()); } @Override public boolean isMember(final Context context, final String groupName) throws SQLException { // special, everyone is member of group 0 (anonymous) if (StringUtils.equals(groupName, Group.ANONYMOUS)) { return true; } else if (context.getCurrentUser() != null) { EPerson currentUser = context.getCurrentUser(); //First check the special groups List<Group> specialGroups = context.getSpecialGroups(); if (CollectionUtils.isNotEmpty(specialGroups)) { for (Group specialGroup : specialGroups) { //Check if the current special group is the one we are looking for OR retrieve all groups & make a check here. if (StringUtils.equals(specialGroup.getName(), groupName) || allMemberGroups(context, currentUser).contains(findByName(context, groupName))) { return true; } } } //lookup eperson in normal groups and subgroups return epersonInGroup(context, groupName, currentUser); } else { return false; } } @Override public List<Group> allMemberGroups(Context context, EPerson ePerson) throws SQLException { Set<Group> groups = new HashSet<>(); if (ePerson != null) { // two queries - first to get groups eperson is a member of // second query gets parent groups for groups eperson is a member of groups.addAll(groupDAO.findByEPerson(context, ePerson)); } // Also need to get all "Special Groups" user is a member of! // Otherwise, you're ignoring the user's membership to these groups! // However, we only do this is we are looking up the special groups // of the current user, as we cannot look up the special groups // of a user who is not logged in. if ((context.getCurrentUser() == null) || (context.getCurrentUser().equals(ePerson))) { List<Group> specialGroups = context.getSpecialGroups(); for(Group special : specialGroups) { groups.add(special); } } // all the users are members of the anonymous group groups.add(findByName(context, Group.ANONYMOUS)); List<Group2GroupCache> groupCache = group2GroupCacheDAO.findByChildren(context, groups); // now we have all owning groups, also grab all parents of owning groups // yes, I know this could have been done as one big query and a union, // but doing the Oracle port taught me to keep to simple SQL! for (Group2GroupCache group2GroupCache : groupCache) { groups.add(group2GroupCache.getParent()); } return new ArrayList<>(groups); } @Override public List<EPerson> allMembers(Context c, Group g) throws SQLException { // two queries - first to get all groups which are a member of this group // second query gets all members of each group in the first query // Get all groups which are a member of this group List<Group2GroupCache> group2GroupCaches = group2GroupCacheDAO.findByParent(c, g); Set<Group> groups = new HashSet<>(); for (Group2GroupCache group2GroupCache : group2GroupCaches) { groups.add(group2GroupCache.getChild()); } Set<EPerson> childGroupChildren = new HashSet<>(ePersonService.findByGroups(c, groups)); //Don't forget to add our direct children childGroupChildren.addAll(g.getMembers()); return new ArrayList<>(childGroupChildren); } @Override public Group find(Context context, UUID id) throws SQLException { if (id == null) { return null; } else { return groupDAO.findByID(context, Group.class, id); } } @Override public Group findByName(Context context, String name) throws SQLException { if (name == null) { return null; } return groupDAO.findByName(context, name); } /** DEPRECATED: Please use {@code findAll(Context context, List<MetadataField> metadataSortFields)} instead */ @Override @Deprecated public List<Group> findAll(Context context, int sortField) throws SQLException { if (sortField == GroupService.NAME) { return findAll(context, null); } else { throw new UnsupportedOperationException("You can only find all groups sorted by name with this method"); } } @Override public List<Group> findAll(Context context, List<MetadataField> metadataSortFields) throws SQLException { return findAll(context, metadataSortFields, -1, -1); } @Override public List<Group> findAll(Context context, List<MetadataField> metadataSortFields, int pageSize, int offset) throws SQLException { if (CollectionUtils.isEmpty(metadataSortFields)) { return groupDAO.findAll(context, pageSize, offset); } else { return groupDAO.findAll(context, metadataSortFields, pageSize, offset); } } @Override public List<Group> search(Context context, String groupIdentifier) throws SQLException { return search(context, groupIdentifier, -1, -1); } @Override public List<Group> search(Context context, String groupIdentifier, int offset, int limit) throws SQLException { List<Group> groups = new ArrayList<>(); UUID uuid = UUIDUtils.fromString(groupIdentifier); if (uuid == null) { //Search by group name groups = groupDAO.findByNameLike(context, groupIdentifier, offset, limit); } else { //Search by group id Group group = find(context, uuid); if (group != null) { groups.add(group); } } return groups; } @Override public int searchResultCount(Context context, String groupIdentifier) throws SQLException { int result = 0; UUID uuid = UUIDUtils.fromString(groupIdentifier); if (uuid == null && StringUtils.isNotBlank(groupIdentifier)) { //Search by group name result = groupDAO.countByNameLike(context, groupIdentifier); } else { //Search by group id Group group = find(context, uuid); if (group != null) { result = 1; } } return result; } @Override public void delete(Context context, Group group) throws SQLException { if (group.isPermanent()) { log.error("Attempt to delete permanent Group $", group.getName()); throw new SQLException("Attempt to delete a permanent Group"); } context.addEvent(new Event(Event.DELETE, Constants.GROUP, group.getID(), group.getName(), getIdentifiers(context, group))); //Remove the supervised group from any workspace items linked to us. group.getSupervisedItems().clear(); // Remove any ResourcePolicies that reference this group authorizeService.removeGroupPolicies(context, group); group.getMemberGroups().clear(); group.getParentGroups().clear(); //Remove all eperson references from this group Iterator<EPerson> ePeople = group.getMembers().iterator(); while (ePeople.hasNext()) { EPerson ePerson = ePeople.next(); ePeople.remove(); ePerson.getGroups().remove(group); } // empty out group2groupcache table (if we do it after we delete our object we get an issue with references) group2GroupCacheDAO.deleteAll(context); // Remove ourself groupDAO.delete(context, group); rethinkGroupCache(context, false); log.info(LogManager.getHeader(context, "delete_group", "group_id=" + group.getID())); } @Override public int getSupportsTypeConstant() { return Constants.GROUP; } /** * Return true if group has no direct or indirect members */ @Override public boolean isEmpty(Group group) { // the only fast check available is on epeople... boolean hasMembers = (!group.getMembers().isEmpty()); if (hasMembers) { return false; } else { // well, groups is never null... for (Group subGroup : group.getMemberGroups()){ hasMembers = !isEmpty(subGroup); if (hasMembers){ return false; } } return !hasMembers; } } @Override public void initDefaultGroupNames(Context context) throws SQLException, AuthorizeException { GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); // Check for Anonymous group. If not found, create it Group anonymousGroup = groupService.findByName(context, Group.ANONYMOUS); if (anonymousGroup==null) { anonymousGroup = groupService.create(context); anonymousGroup.setName(Group.ANONYMOUS); anonymousGroup.setPermanent(true); groupService.update(context, anonymousGroup); } // Check for Administrator group. If not found, create it Group adminGroup = groupService.findByName(context, Group.ADMIN); if (adminGroup == null) { adminGroup = groupService.create(context); adminGroup.setName(Group.ADMIN); adminGroup.setPermanent(true); groupService.update(context, adminGroup); } } /** * Get a list of groups with no members. * * @param context * The relevant DSpace Context. * @return list of groups with no members * @throws SQLException * An exception that provides information on a database access error or other errors. */ @Override public List<Group> getEmptyGroups(Context context) throws SQLException { return groupDAO.getEmptyGroups(context); } /** * Update the group - writing out group object and EPerson list if necessary * * @param context * The relevant DSpace Context. * @param group * Group to update * @throws SQLException * An exception that provides information on a database access error or other errors. * @throws AuthorizeException * Exception indicating the current user of the context does not have permission * to perform a particular action. */ @Override public void update(Context context, Group group) throws SQLException, AuthorizeException { super.update(context, group); // FIXME: Check authorisation groupDAO.save(context, group); if (group.isMetadataModified()) { context.addEvent(new Event(Event.MODIFY_METADATA, Constants.GROUP, group.getID(), group.getDetails(), getIdentifiers(context, group))); group.clearDetails(); } if (group.isGroupsChanged()) { rethinkGroupCache(context, true); group.clearGroupsChanged(); } log.info(LogManager.getHeader(context, "update_group", "group_id=" + group.getID())); } protected boolean epersonInGroup(Context context, String groupName, EPerson ePerson) throws SQLException { return groupDAO.findByNameAndMembership(context, groupName, ePerson) != null; } /** * Regenerate the group cache AKA the group2groupcache table in the database - * meant to be called when a group is added or removed from another group * * @param context * The relevant DSpace Context. * @param flushQueries * flushQueries Flush all pending queries * @throws SQLException * An exception that provides information on a database access error or other errors. */ protected void rethinkGroupCache(Context context, boolean flushQueries) throws SQLException { Map<UUID, Set<UUID>> parents = new HashMap<>(); List<Pair<UUID, UUID>> group2groupResults = groupDAO.getGroup2GroupResults(context, flushQueries); for (Pair<UUID, UUID> group2groupResult : group2groupResults) { UUID parent = group2groupResult.getLeft(); UUID child = group2groupResult.getRight(); // if parent doesn't have an entry, create one if (!parents.containsKey(parent)) { Set<UUID> children = new HashSet<>(); // add child id to the list children.add(child); parents.put(parent, children); } else { // parent has an entry, now add the child to the parent's record // of children Set<UUID> children = parents.get(parent); children.add(child); } } // now parents is a hash of all of the IDs of groups that are parents // and each hash entry is a hash of all of the IDs of children of those // parent groups // so now to establish all parent,child relationships we can iterate // through the parents hash for (Map.Entry<UUID, Set<UUID>> parent : parents.entrySet()) { Set<UUID> myChildren = getChildren(parents, parent.getKey()); parent.getValue().addAll(myChildren); } // empty out group2groupcache table group2GroupCacheDAO.deleteAll(context); // write out new one for (Map.Entry<UUID, Set<UUID>> parent : parents.entrySet()) { UUID key = parent.getKey(); for (UUID child : parent.getValue()) { Group parentGroup = find(context, key); Group childGroup = find(context, child); if (parentGroup != null && childGroup != null && group2GroupCacheDAO.find(context, parentGroup, childGroup) == null) { Group2GroupCache group2GroupCache = group2GroupCacheDAO.create(context, new Group2GroupCache()); group2GroupCache.setParent(parentGroup); group2GroupCache.setChild(childGroup); group2GroupCacheDAO.save(context, group2GroupCache); } } } } @Override public DSpaceObject getParentObject(Context context, Group group) throws SQLException { if (group == null) { return null; } // could a collection/community administrator manage related groups? // check before the configuration options could give a performance gain // if all group management are disallowed if (AuthorizeConfiguration.canCollectionAdminManageAdminGroup() || AuthorizeConfiguration.canCollectionAdminManageSubmitters() || AuthorizeConfiguration.canCollectionAdminManageWorkflows() || AuthorizeConfiguration.canCommunityAdminManageAdminGroup() || AuthorizeConfiguration .canCommunityAdminManageCollectionAdminGroup() || AuthorizeConfiguration .canCommunityAdminManageCollectionSubmitters() || AuthorizeConfiguration .canCommunityAdminManageCollectionWorkflows()) { // is this a collection related group? org.dspace.content.Collection collection = collectionService.findByGroup(context, group); if (collection != null) { if ((group.equals(collection.getWorkflowStep1()) || group.equals(collection.getWorkflowStep2()) || group.equals(collection.getWorkflowStep3()))) { if (AuthorizeConfiguration.canCollectionAdminManageWorkflows()) { return collection; } else if (AuthorizeConfiguration.canCommunityAdminManageCollectionWorkflows()) { return collectionService.getParentObject(context, collection); } } if (group.equals(collection.getSubmitters())) { if (AuthorizeConfiguration.canCollectionAdminManageSubmitters()) { return collection; } else if (AuthorizeConfiguration.canCommunityAdminManageCollectionSubmitters()) { return collectionService.getParentObject(context, collection); } } if (group.equals(collection.getAdministrators())) { if (AuthorizeConfiguration.canCollectionAdminManageAdminGroup()) { return collection; } else if (AuthorizeConfiguration.canCommunityAdminManageCollectionAdminGroup()) { return collectionService.getParentObject(context, collection); } } } // is the group related to a community and community administrator allowed // to manage it? else if (AuthorizeConfiguration.canCommunityAdminManageAdminGroup()) { return communityService.findByAdminGroup(context, group); } } return null; } @Override public void updateLastModified(Context context, Group dso) { //Not needed. } /** * Used recursively to generate a map of ALL of the children of the given * parent * * @param parents * Map of parent,child relationships * @param parent * the parent you're interested in * @return Map whose keys are all of the children of a parent */ protected Set<UUID> getChildren(Map<UUID,Set<UUID>> parents, UUID parent) { Set<UUID> myChildren = new HashSet<>(); // degenerate case, this parent has no children if (!parents.containsKey(parent)) { return myChildren; } // got this far, so we must have children Set<UUID> children = parents.get(parent); // now iterate over all of the children for (UUID child : children) { // add this child's ID to our return set myChildren.add(child); // and now its children myChildren.addAll(getChildren(parents, child)); } return myChildren; } @Override public Group findByIdOrLegacyId(Context context, String id) throws SQLException { if (org.apache.commons.lang.StringUtils.isNumeric(id)) { return findByLegacyId(context, Integer.parseInt(id)); } else { return find(context, UUIDUtils.fromString(id)); } } @Override public Group findByLegacyId(Context context, int id) throws SQLException { return groupDAO.findByLegacyId(context, id, Group.class); } @Override public int countTotal(Context context) throws SQLException { return groupDAO.countRows(context); } @Override public List<Group> findByMetadataField(final Context context, final String searchValue, final MetadataField metadataField) throws SQLException { return groupDAO.findByMetadataField(context, searchValue, metadataField); } }