org.sakaiproject.entitybroker.providers.MembershipEntityProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.entitybroker.providers.MembershipEntityProvider.java

Source

/**
 * $Id$
 * $URL$
 * ServerConfigEntityProvider.java - entity-broker - Jul 17, 2008 2:19:03 PM - azeckoski
 **************************************************************************
 * Copyright (c) 2008, 2009 The Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.sakaiproject.entitybroker.providers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.api.privacy.PrivacyManager;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.Member;
import org.sakaiproject.authz.api.Role;
import org.sakaiproject.email.api.EmailService;
import org.sakaiproject.entitybroker.EntityReference;
import org.sakaiproject.entitybroker.EntityView;
import org.sakaiproject.entitybroker.entityprovider.CoreEntityProvider;
import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction;
import org.sakaiproject.entitybroker.entityprovider.capabilities.ActionsExecutable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.CollectionResolvable;
import org.sakaiproject.entitybroker.entityprovider.capabilities.RESTful;
import org.sakaiproject.entitybroker.entityprovider.extension.ActionReturn;
import org.sakaiproject.entitybroker.entityprovider.extension.EntityData;
import org.sakaiproject.entitybroker.entityprovider.extension.Formats;
import org.sakaiproject.entitybroker.entityprovider.search.Order;
import org.sakaiproject.entitybroker.entityprovider.search.Restriction;
import org.sakaiproject.entitybroker.entityprovider.search.Search;
import org.sakaiproject.entitybroker.exception.EntityNotFoundException;
import org.sakaiproject.entitybroker.providers.model.EntityMember;
import org.sakaiproject.entitybroker.providers.model.EntityUser;
import org.sakaiproject.entitybroker.util.AbstractEntityProvider;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.site.api.Group;
import org.sakaiproject.site.api.Site;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.site.api.SiteService.SelectionType;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * This provides access to memberships as entities
 * 
 * @author Aaron Zeckoski (azeckoski @ gmail.com)
 */
public class MembershipEntityProvider extends AbstractEntityProvider
        implements CoreEntityProvider, RESTful, ActionsExecutable {

    private static Log log = LogFactory.getLog(MembershipEntityProvider.class);

    private SiteService siteService;
    private AuthzGroupService authzGroupService;

    public void setAuthzGroupService(AuthzGroupService authzGroupService) {
        this.authzGroupService = authzGroupService;
    }

    public void setSiteService(SiteService siteService) {
        this.siteService = siteService;
    }

    private UserEntityProvider userEntityProvider;

    public void setUserEntityProvider(UserEntityProvider userEntityProvider) {
        this.userEntityProvider = userEntityProvider;
    }

    private EmailService emailService;

    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }

    private PrivacyManager privacyManager;

    public void setPrivacyManager(PrivacyManager privacyManager) {
        this.privacyManager = privacyManager;
    }

    private SecurityService securityService;

    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public static String PREFIX = "membership";

    public String getEntityPrefix() {
        return PREFIX;
    }

    private static final String GROUP_PROP_WSETUP_CREATED = "group_prop_wsetup_created";
    private static final String ADMIN_SITE_ID = "!admin";
    /**
     * SAKAI CONFIG
     * False by default, no admin site changes allowed
     */
    private static final String ADMIN_SITE_CHANGE_ALLOWED = "eb.membership.admin.site.changes.allowed";
    private boolean allowAdminSiteChanges = false;

    public void init() {
        allowAdminSiteChanges = developerHelperService.getConfigurationSetting(ADMIN_SITE_CHANGE_ALLOWED, false);
    }

    /**
     * join/site/siteId or join/siteId Handle the special case of joining a site, using normal
     * create will not work
     */
    @EntityCustomAction(action = "join", viewKey = EntityView.VIEW_NEW)
    public boolean joinCurrentUserToSite(EntityView view, Map<String, Object> params) {
        String siteId = view.getPathSegment(2);
        if (siteId == null) {
            siteId = (String) params.get("siteId");
        } else if ("site".equals(siteId)) {
            siteId = view.getPathSegment(3);
        }
        if (siteId == null) {
            throw new IllegalArgumentException(
                    "siteId must be set in order join sites, set in params or in the URL /join/site/siteId");
        }
        checkSiteSecurity(siteId);
        try {
            siteService.join(siteId);
        } catch (IdUnusedException e) {
            throw new IllegalArgumentException("The siteId provided (" + siteId + ") could not be found: " + e, e);
        } catch (PermissionException e) {
            throw new SecurityException("The current user (" + developerHelperService.getCurrentUserId()
                    + ") does not have permission to join site (" + siteId + "): " + e, e);
        }
        return true;
    }

    /**
     * unjoin/site/siteId or unjoin/siteId Handle the special case of un-joining a site, using
     * normal delete will not work
     */
    @EntityCustomAction(action = "unjoin", viewKey = EntityView.VIEW_NEW)
    public boolean unjoinCurrentUserFromSite(EntityView view, Map<String, Object> params) {
        String siteId = view.getPathSegment(2);
        if (siteId == null) {
            siteId = (String) params.get("siteId");
        } else if ("site".equals(siteId)) {
            siteId = view.getPathSegment(3);
        }
        if (siteId == null) {
            throw new IllegalArgumentException(
                    "siteId must be set in order to unjoin sites, set in params or in the URL /unjoin/site/siteId");
        }
        checkSiteSecurity(siteId);
        try {
            siteService.unjoin(siteId);
        } catch (IdUnusedException e) {
            throw new IllegalArgumentException("The siteId provided (" + siteId + ") could not be found: " + e, e);
        } catch (PermissionException e) {
            throw new SecurityException("The current user (" + developerHelperService.getCurrentUserId()
                    + ") does not have permission to join site (" + siteId + "): " + e, e);
        }
        return true;
    }

    /**
     * Handle the special needs of UX site membership settings, either getting the current list of
     * site memberships via a GET request, or creating a new batch of site memberships via a POST
     * request. In the case of a POST, special HTTP response headers will be used to communicate
     * success or warning conditions to the client.
     */
    @EntityCustomAction(action = "site", viewKey = "")
    public ActionReturn handleSiteMemberships(EntityView view, Map<String, Object> params) {
        if (log.isDebugEnabled())
            log.debug("handleSiteMemberships method=" + view.getMethod() + ", params=" + params);
        String siteId = view.getPathSegment(2);
        if (siteId == null) {
            siteId = (String) params.get("siteId");
            if (siteId == null) {
                throw new IllegalArgumentException(
                        "siteId must be set in order to get site memberships, set in params or in the URL /membership/site/siteId");
            }
        }

        String locationReference = "/site/" + siteId;

        Map<String, String> extraResponseHeaders = null;
        if (EntityView.Method.POST.name().equals(view.getMethod())) {
            extraResponseHeaders = createBatchMemberships(view, params, locationReference);
        }

        List<EntityData> l = getEntities(new EntityReference(PREFIX, ""),
                new Search(CollectionResolvable.SEARCH_LOCATION_REFERENCE, locationReference));
        ActionReturn actionReturn = new ActionReturn(l, Formats.JSON);
        if ((extraResponseHeaders != null) && !extraResponseHeaders.isEmpty()) {
            actionReturn.setHeaders(extraResponseHeaders);
        }
        return actionReturn;
    }

    /**
     * Add members to a site.
     * 
     * @param view
     * @param params
     *            request parameters including a list of userSearchValues
     * @param locationReference
     * @return headers containing success or warning messages for the client
     */
    public Map<String, String> createBatchMemberships(EntityView view, Map<String, Object> params,
            String locationReference) {

        SiteGroup sg = findLocationByReference(locationReference);
        String roleId = (String) params.get("memberRole");
        String notificationMessage = (String) params.get("notificationMessage");
        if ((notificationMessage != null) && (notificationMessage.trim().length() == 0)) {
            notificationMessage = null;
        }
        boolean active = true;

        Map<String, String> responseHeaders = new HashMap<String, String>();
        Set<EntityUser> users = new HashSet<EntityUser>();
        Set<String> valuesNotFound = new HashSet<String>();
        Set<String> valuesAlreadyMembers = new HashSet<String>();
        List<String> userSearchValues = getListFromValue(params.get("userSearchValues"));
        for (String userSearchValue : userSearchValues) {
            EntityUser user = userEntityProvider.findUserFromSearchValue(userSearchValue);
            if (user != null) {
                if (sg.site.getUserRole(user.getId()) != null) {
                    valuesAlreadyMembers.add(userSearchValue);
                } else {
                    users.add(user);
                }
            } else {
                valuesNotFound.add(userSearchValue);
            }
        }

        if (!users.isEmpty()) {
            String currentUserEmail = userEntityProvider.getCurrentUser(null).getEmail();
            for (EntityUser user : users) {
                sg.site.addMember(user.getId(), roleId, active, false);
                if (notificationMessage != null) {
                    /**
                     * TODO Should the From address be the site contact or the "setup.request" Sakai
                     * property? TODO We need to retrieve a localized message title and additional
                     * body (if any) instead of hard-coding it. See the new Email Template Service
                     * for a likely approach.
                     */
                    emailService.send(currentUserEmail, user.getEmail(), "New Site Membership Notification",
                            notificationMessage, null, null, null);
                }
            }
            saveSiteMembership(sg.site);
            responseHeaders.put("x-success-count", String.valueOf(users.size()));
        }
        if (!valuesNotFound.isEmpty()) {
            Iterator<String> listIter = valuesNotFound.iterator();
            StringBuilder listString = new StringBuilder(listIter.next());
            while (listIter.hasNext()) {
                listString.append(", ").append(listIter.next());
            }
            responseHeaders.put("x-warning-not-found", listString.toString());
        }
        if (!valuesAlreadyMembers.isEmpty()) {
            Iterator<String> listIter = valuesAlreadyMembers.iterator();
            StringBuilder listString = new StringBuilder(listIter.next());
            while (listIter.hasNext()) {
                listString.append(", ").append(listIter.next());
            }
            responseHeaders.put("x-warning-already-members", listString.toString());
        }
        return responseHeaders;
    }

    @EntityCustomAction(action = "fastroles", viewKey = "")
    public List<EntityData> getMembershipRoles(EntityView view, Map<String, Object> params) {
        //Can be ID or EID
        String userId = view.getPathSegment(2);
        //Don't include member details
        Search s = new Search("includeMemberDetails", false);
        if (userId != null) {
            s.addRestriction(new Restriction(CollectionResolvable.SEARCH_USER_REFERENCE, userId));
        }
        return getEntities(new EntityReference(PREFIX, ""), s);

    }

    /**
     * Special handler for JSON uploads of site membership data. Takes a parameter 'json' which
     * should contain json of the form:
     * 
     * [ {"id": "user1", "role": "access"},{"id": "user2","role": "maintain"} ]
     */
    @EntityCustomAction(action = "sitebyjson", viewKey = EntityView.VIEW_NEW)
    public ActionReturn handleSiteJsonUpload(EntityView view, Map<String, Object> params) {

        if (log.isDebugEnabled()) {
            log.debug("handleSiteJsonUpload method=" + view.getMethod() + ", params=" + params);
        }

        String siteId = view.getPathSegment(2);
        if (siteId == null) {
            siteId = (String) params.get("siteId");
            if (siteId == null) {
                throw new IllegalArgumentException(
                        "siteId must be set in order to get site memberships, set in params or in the URL /membership/site/siteId");
            }
        }

        String locationReference = "/site/" + siteId;

        String json = (String) params.get("json");
        if (json == null) {
            throw new IllegalArgumentException(
                    "The membership JSON data must be supplied as a POST parameter named 'json'.");
        }

        Map<String, String> extraResponseHeaders = new HashMap<String, String>(3);

        ObjectMapper mapper = new ObjectMapper();
        List<JsonUser> memberships;

        try {
            memberships = mapper.readValue(json.getBytes(), new TypeReference<List<JsonUser>>() {
            });
        } catch (JsonParseException jpe) {
            throw new IllegalArgumentException(
                    "The supplied JSON was invalid. Have a look at http://www.json.org/.");
        } catch (JsonMappingException jpe) {
            throw new IllegalArgumentException(
                    "The supplied JSON was invalid. Take a look at /direct/membership/describe for the correct structure.");
        } catch (IOException e) {
            throw new IllegalArgumentException("Failed to read the supplied JSON.");
        }

        // Collect the users into roles
        Map<String, List<String>> usersToRoleMap = new HashMap<String, List<String>>();
        for (JsonUser user : memberships) {
            String id = user.getId();
            String role = user.getRole();

            if (usersToRoleMap.containsKey(role)) {
                usersToRoleMap.get(role).add(id);
            } else {
                List<String> users = new ArrayList<String>();
                users.add(id);
                usersToRoleMap.put(role, users);
            }
        }

        Map<String, Object> localParams = new HashMap<String, Object>(2);

        // Compile results from createBatchMemberships. We'll return these in the response.
        int successCount = 0;
        StringBuilder notFoundBuilder = new StringBuilder();
        StringBuilder alreadyMemberBuilder = new StringBuilder();

        Iterator<String> memberRoles = usersToRoleMap.keySet().iterator();
        while (memberRoles.hasNext()) {
            String memberRole = memberRoles.next();
            localParams.put("userSearchValues", usersToRoleMap.get(memberRole));
            localParams.put("memberRole", memberRole);
            Map<String, String> response = createBatchMemberships(view, localParams, locationReference);

            String successCountString = response.get("x-success-count");
            if (successCountString != null) {
                try {
                    successCount += Integer.parseInt(successCountString);
                } catch (NumberFormatException nfe) {
                    log.error("x-success-count was not a number. successCount was not increased.");
                }
            }

            String nf = response.get("x-warning-not-found");
            if (nf != null) {
                notFoundBuilder.append(nf);
                if (memberRoles.hasNext()) {
                    notFoundBuilder.append(", ");
                }
            }

            String am = response.get("x-warning-already-members");
            if (am != null) {
                alreadyMemberBuilder.append(am);
                if (memberRoles.hasNext()) {
                    alreadyMemberBuilder.append(", ");
                }
            }
        }

        extraResponseHeaders.put("x-success-count", String.valueOf(successCount));
        extraResponseHeaders.put("x-warning-not-found", notFoundBuilder.toString());
        extraResponseHeaders.put("x-warning-already-members", alreadyMemberBuilder.toString());

        return new ActionReturn("", extraResponseHeaders);
    }

    @EntityCustomAction(action = "group", viewKey = "")
    public List<EntityData> getGroupMemberships(EntityView view, Map<String, Object> params) {
        String groupId = view.getPathSegment(2);
        List<EntityData> ed = null;

        if (EntityView.Method.GET.name().equals(view.getMethod())) {
            // GET /direct/membership/group/groupid - gets current membership for the given groupid

            if (groupId == null) {
                groupId = (String) params.get("groupId");
                if (groupId == null) {
                    throw new IllegalArgumentException(
                            "groupId must be set in order to get group memberships, set in params or in the URL /membership/group/groupId");
                }
            }
            ed = getEntities(new EntityReference(PREFIX, ""),
                    new Search(CollectionResolvable.SEARCH_LOCATION_REFERENCE, "/group/" + groupId));

        } else if (EntityView.Method.POST.name().equals(view.getMethod())) {
            // POST /direct/membership/group/groupid - update the membership for the given groupid

            String action = params.get("action") != null ? params.get("action").toString() : null;

            if (action == null || "".equals(action)) {
                throw new IllegalArgumentException(
                        "A parameter named 'action' needs to be specified. 'action' can be update, add or remove. Cannot edit group:"
                                + groupId);
            }

            List<String> userIds = params.get("userIds") != null
                    ? Arrays.asList(params.get("userIds").toString().split(","))
                    : new ArrayList<String>();
            if (userIds.size() <= 0) {
                throw new IllegalArgumentException(
                        "A list of user ids needs to be specified as a parameter named 'userIds'. Cannot edit group:"
                                + groupId);
            }

            SiteGroup siteGroup = findLocationByReference("/group/" + groupId);
            Site site = siteGroup.site;
            Group group = siteGroup.group;

            if (site == null) {
                throw new IllegalArgumentException("The site for the group (" + groupId + ") could not be found.");
            }
            if (group == null) {
                throw new IllegalArgumentException("The group provided (" + groupId + ") could not be found.");
            }

            checkGroupType(group);

            if (!siteService.allowUpdateSite(site.getId())) {
                throw new SecurityException(
                        "This site (" + site.getReference() + ") cannot be updated by the current user.");
            }

            if ("add".equals(action)) {
                // add the list to the existing membership
                for (String user : userIds) {
                    String userId = userEntityProvider.findAndCheckUserId(null, user.trim());
                    if (userId == null) {
                        log.warn("Unable to add user (" + user + ") to group (" + group.getId() + ") in site ("
                                + site.getId() + "), could not find user record by id or eid");
                        continue;
                    }
                    Member m = site.getMember(userId);
                    if (m == null) {
                        log.warn("Unable to add user (" + user + ") to group (" + group.getId() + ") in site ("
                                + site.getId() + "), user is not a member of the site (and must be)");
                        continue;
                    }
                    Role role = m.getRole();

                    if (group.getMember(userId) == null && (role != null && role.getId() != null)) {
                        // Every user added via this EB is defined as non-provided
                        group.addMember(userId, role.getId(), m != null ? m.isActive() : true, false);
                    }
                }
            } else if ("update".equals(action)) {
                if (!siteService.allowUpdateGroupMembership(site.getId())) {
                    throw new SecurityException("This group (" + groupId + ") in site (" + site.getId()
                            + ") cannot be updated by the current user.");
                }
                // replace the current membership with the provided list
                group.removeMembers();
                for (String user : userIds) {
                    String userId = userEntityProvider.findAndCheckUserId(null, user.trim());
                    if (userId == null) {
                        log.warn("Unable to update user (" + user + ") in group (" + group.getId() + ") in site ("
                                + site.getId() + "), could not find user record by id or eid");
                        continue;
                    }
                    Member m = site.getMember(userId);
                    Role role = m.getRole();

                    if (group.getMember(userId) == null && (role != null && role.getId() != null)) {
                        // Every user added via this EB is defined as non-provided
                        group.addMember(userId, role.getId(), m != null ? m.isActive() : true, false);
                    }
                }
            } else if ("remove".equals(action)) {
                // remove the list from the existing membership
                for (String userId : userIds) {
                    userId = userEntityProvider.findAndCheckUserId(null, userId.trim());
                    if (userId == null) {
                        log.warn("Unable to remove user (" + userId + ") from group (" + group.getId()
                                + ") in site (" + site.getId() + "), could not find user record by id or eid");
                        continue;
                    }
                    group.removeMember(userId);
                }
            } else {
                throw new IllegalArgumentException(
                        "A valid value for the parameter named 'action' needs to be specified. 'action' can be update, add or remove. Cannot edit group:"
                                + groupId);
            }

            // save group
            try {
                siteService.save(site);
            } catch (IdUnusedException e) {
                throw new IllegalArgumentException(
                        "Cannot find site with given id: " + site.getId() + ":" + e.getMessage(), e);
            } catch (PermissionException e) {
                throw new SecurityException("Current user does not have permission to save this group:" + groupId
                        + " to site:" + site.getId());
            }
            return null;
        }
        return ed;
    }

    public boolean entityExists(String id) {
        if (id == null) {
            return false;
        }
        if ("".equals(id)) {
            return true;
        }
        String[] parts = EntityMember.parseId(id);
        if (parts != null) {
            // TODO check it later when there is an efficient way
            return true;
        }
        return false;
    }

    public Object getEntity(EntityReference ref) {
        if (ref.getId() == null) {
            return new EntityMember();
        }
        String mid = ref.getId();
        String[] parts = EntityMember.parseId(mid);
        if (parts == null) {
            throw new IllegalArgumentException("Invalid membership id (" + mid
                    + "), should be formed like so: 'userId::site:siteId' or 'userId::group:groupId");
        }
        EntityMember member = getMember(parts[0], parts[1]);
        if (member == null) {
            throw new IllegalArgumentException("Cannot find membership with id: " + mid);
        }
        return member;
    }

    /**
     * Gets the list of all memberships for the current user if no params provided, otherwise gets
     * memberships in a specified location or for a specified user
     */
    public List<EntityData> getEntities(EntityReference ref, Search search) {
        String currentUserId = developerHelperService.getCurrentUserId();
        String userId = null;
        String locationReference = null;
        String roleId = null;
        boolean includeSites = true;
        boolean includeGroups = false;
        //Include details about the membership, has a performance impact
        boolean includeMemberDetails = true;

        //SAK-25710 hold a map of each sites type so we can look them up later (entityId, siteType)
        Map<String, String> siteTypes = new HashMap<String, String>();

        if (search == null) {
            search = new Search();
        }
        if (!search.isEmpty()) {
            // process the search
            roleId = (String) search.getRestrictionValueByProperties(new String[] { "role", "roleId" });
            Restriction userRes = search.getRestrictionByProperty(CollectionResolvable.SEARCH_USER_REFERENCE);
            if (userRes != null) {
                String userRef = userRes.getStringValue();
                userId = EntityReference.getIdFromRef(userRef);
            }
            Restriction locRes = search.getRestrictionByProperty(CollectionResolvable.SEARCH_LOCATION_REFERENCE);
            if (locRes != null) {
                locationReference = locRes.getStringValue();
            }
            Restriction incSites = search.getRestrictionByProperty("includeSites");
            if (incSites != null) {
                includeSites = incSites.getBooleanValue();
            }
            Restriction incGroups = search.getRestrictionByProperty("includeGroups");
            if (incGroups != null) {
                includeGroups = incGroups.getBooleanValue();
            }

            Restriction incMemberDetails = search.getRestrictionByProperty("includeMemberDetails");
            if (incMemberDetails != null) {
                includeMemberDetails = incMemberDetails.getBooleanValue();
            }
        }
        if (locationReference == null && userId == null) {
            // if these are both null then we default to getting memberships for the current user
            if (currentUserId != null) {
                userId = currentUserId;
            }
        }
        if (locationReference == null && userId == null) {
            // fail if there is still nothing to output
            throw new IllegalArgumentException("There must be a current user logged in "
                    + "OR you must provide a search with the following restrictions (getting all is not supported): "
                    + "siteId, locationReference, groupId AND (optionally) roleId OR userReference, userId, user");
        }
        List<EntityMember> members = new ArrayList<EntityMember>();
        boolean findByLocation = false;
        if (locationReference != null) {
            // get membership for a location
            findByLocation = true;
            members = getMembers(locationReference);
        } else {
            // get membership for
            if (!includeGroups && !includeSites) {
                throw new IllegalArgumentException("includesSites and includesGroups cannot both be false");
            }
            // find memberships by userId
            userId = userEntityProvider.findAndCheckUserId(userId, null);
            //SAK-22396 if the user is unknown this will be null
            if (userId == null) {
                throw new IllegalArgumentException("unable to find user with id (" + userId + ")");
            }

            boolean userCurrent = userId.equals(currentUserId);
            if (!userCurrent && !developerHelperService.isUserAdmin(currentUserId)) {
                throw new SecurityException("Only admin can access other user memberships, current user ("
                        + currentUserId + ") cannot access ref: " + userId);
            }

            // Is there a faster way to do this? I really truly hope so -AZ
            // Only if you don't care about getMember details -MJ
            try {
                if (!userCurrent) {
                    developerHelperService.setCurrentUser("/user/" + userId);
                }
                List<Site> sites = siteService.getSites(SelectionType.ACCESS, null, null, null, null, null);
                if (includeMemberDetails) {
                    for (Site site : sites) {
                        Member sm = site.getMember(userId);
                        if (sm != null) {
                            if (includeSites) {
                                EntityMember em = new EntityMember(sm, site.getReference(), null);
                                members.add(em);
                                siteTypes.put(em.getId(), site.getType());
                            }
                            // also check the groups
                            if (includeGroups) {
                                Collection<Group> groups = site.getGroups();
                                for (Group group : groups) {
                                    Member gm = group.getMember(userId);
                                    if (gm != null) {
                                        members.add(new EntityMember(gm, group.getReference(), null));
                                    }
                                }
                            }
                        }
                    }
                } else {
                    Map<String, String> userRoles = authzGroupService.getUserRoles(userId, null);
                    for (Site site : sites) {
                        EntityMember em = new EntityMember(userId, site.getReference(),
                                userRoles.get(site.getReference()), true, null);
                        members.add(em);
                    }
                }
            } finally {
                if (!userCurrent) {
                    developerHelperService.restoreCurrentUser();
                }
            }
        }
        ArrayList<EntityMember> sortedMembers = new ArrayList<EntityMember>();
        int count = 0;
        for (EntityMember em : members) {
            // filter out users and roles
            if (count < search.getStart()) {
                continue;
            } else if (search.getLimit() > 0 && count > search.getLimit()) {
                break; // no more, limit reached
            } else {
                // between the start and limit
                if (roleId != null) {
                    if (!roleId.equals(em.getMemberRole())) {
                        continue;
                    }
                }
                if (findByLocation) {
                    if (userId != null) {
                        if (!userId.equals(em.getUserId())) {
                            continue;
                        }
                    }
                }
                sortedMembers.add(em);
            }
            count++;
        }
        // handle the sorting
        Comparator<EntityMember> memberComparator = new EntityMember.MemberSortName(); // default by
        // sortname
        if (search.getOrders().length > 0) {
            Order order = search.getOrders()[0]; // only one sort allowed
            if ("email".equals(order.getProperty())) {
                memberComparator = new EntityMember.MemberEmail();
            } else if ("displayName".equals(order.getProperty())) {
                memberComparator = new EntityMember.MemberDisplayName();
            } else if ("lastLogin".equals(order.getProperty())) {
                memberComparator = new EntityMember.MemberLastLogin();
            }
        }
        Collections.sort(sortedMembers, memberComparator);
        // TODO reverse sorting?

        // now we put the members into entity data objects
        ArrayList<EntityData> l = new ArrayList<EntityData>();
        for (EntityMember em : sortedMembers) {

            //SAK-25710 add site type as a property
            Map<String, Object> props = new HashMap<String, Object>();
            String siteType = siteTypes.get(em.getId());
            props.put("siteType", siteType);

            EntityData ed = new EntityData(new EntityReference(PREFIX, em.getId()), null, em, props);
            l.add(ed);
        }
        return l;
    }

    public String createEntity(EntityReference ref, Object entity, Map<String, Object> params) {
        SiteGroup sg = null;
        String roleId = null;
        String userId = null;
        boolean active = true;
        if (entity.getClass().isAssignableFrom(Member.class)) {
            // if someone passes in a Member
            Member member = (Member) entity;
            String locationReference = (String) params.get("locationReference");
            if (locationReference == null) {
                throw new IllegalArgumentException(
                        "Cannot create/update a membership entity from Member without a locationReference in the params");
            }
            sg = findLocationByReference(locationReference);
            roleId = member.getRole().getId();
            userId = userEntityProvider.findAndCheckUserId(member.getUserId(), member.getUserEid());
            active = member.isActive();
        } else if (entity.getClass().isAssignableFrom(EntityMember.class)) {
            // if they instead pass in the EntitySite object
            EntityMember em = (EntityMember) entity;
            sg = findLocationByReference(em.getLocationReference());
            roleId = em.getMemberRole();
            if ((em.getUserId() != null) || (em.getUserEid() != null)) {
                userId = userEntityProvider.findAndCheckUserId(em.getUserId(), em.getUserEid());
            }
            active = em.isActive();
        } else {
            throw new IllegalArgumentException(
                    "Invalid entity for create/update, must be Member or EntityMember object");
        }
        if (roleId == null || "".equals(roleId)) {
            roleId = sg.site.getJoinerRole();
        }

        // SAK-21786
        // set the role to the one passed in, if available
        if (params.get("memberRole") != null) {
            roleId = (String) params.get("memberRole");
        }
        // set the active status to the one passed in, if available
        if (params.get("active") != null) {
            active = Boolean.parseBoolean((String) params.get("active"));
        }

        checkSiteSecurity(sg.site.getId());

        // check for a batch add
        String[] userIds = checkForBatch(params, userId);
        // now add all the memberships
        String memberId = "";
        String currentUserId = developerHelperService.getCurrentUserId();
        for (int i = 0; i < userIds.length; i++) {
            if (sg.group == null) {
                // site only
                if (userIds[i].equals(currentUserId) && sg.site.isJoinable()) {
                    try {
                        siteService.join(sg.site.getId());
                    } catch (IdUnusedException e) {
                        throw new IllegalArgumentException(
                                "Invalid site: " + sg.site.getId() + ":" + e.getMessage(), e);
                    } catch (PermissionException e) {
                        throw new SecurityException(
                                "Current user not allowed to join site: " + sg.site.getId() + ":" + e.getMessage(),
                                e);
                    }
                } else {
                    sg.site.addMember(userIds[i], roleId, active, false);
                    saveSiteMembership(sg.site);
                }
            } else {
                // group and site
                sg.group.addMember(userIds[i], roleId, active, false);
                saveGroupMembership(sg.site, sg.group);
            }
            if (i == 0) {
                EntityMember em = new EntityMember(userIds[0], sg.locationReference, roleId, active, null);
                memberId = em.getId();
            }
        }
        if (userIds.length > 1) {
            log.info("Batch add memberships: siteId=" + ((sg.site == null) ? "none" : sg.site.getId()) + ",groupId="
                    + ((sg.group == null) ? "none" : sg.group.getId()) + ",userIds="
                    + Search.arrayToString(userIds));

            memberId = "batch:" + memberId;
        }
        return memberId;
    }

    public Object getSampleEntity() {
        return new EntityMember();
    }

    public void updateEntity(EntityReference ref, Object entity, Map<String, Object> params) {
        // same operation for updating memberships, maybe we should check if they exist?
        createEntity(ref, entity, params);
    }

    public void deleteEntity(EntityReference ref, Map<String, Object> params) {
        String mid = ref.getId();
        String[] parts = EntityMember.parseId(mid);
        if (parts == null) {
            throw new IllegalArgumentException("Invalid membership id (" + mid
                    + "), should be formed like so: 'userId::site:siteId' or 'userId::group:groupId");
        }
        String userId = parts[0];
        SiteGroup sg = findLocationByReference(parts[1]);
        // check for a batch
        String[] userIds = checkForBatch(params, userId);
        for (int i = 0; i < userIds.length; i++) {
            if (sg.group == null) {
                // site only
                sg.site.removeMember(userIds[i]);
                saveSiteMembership(sg.site);
            } else {
                // group and site
                sg.group.removeMember(userIds[i]);
                saveGroupMembership(sg.site, sg.group);
            }
        }
        if (userIds.length > 1) {
            log.info("Batch remove memberships: siteId=" + ((sg.site == null) ? "none" : sg.site.getId())
                    + ",groupId=" + ((sg.group == null) ? "none" : sg.group.getId()) + ",userIds="
                    + Search.arrayToString(userIds));
        }
    }

    public String[] getHandledOutputFormats() {
        return new String[] { Formats.HTML, Formats.XML, Formats.JSON, Formats.FORM };
    }

    public String[] getHandledInputFormats() {
        return new String[] { Formats.HTML, Formats.XML, Formats.JSON };
    }

    public EntityMember getMember(String userId, String locationReference) {
        EntityMember em = null;
        Member member = null;
        SiteGroup sg = findLocationByReference(locationReference);
        String currentUserId = developerHelperService.getCurrentUserId();
        if (!userId.equals(currentUserId)) {
            isAllowedAccessMembers(sg.site, sg.group);
        }
        boolean viewHidden = securityService.unlock("roster.viewHidden", sg.site.getReference());
        if (sg.group == null) {
            // site only
            member = sg.site.getMember(userId);
        } else {
            // group and site
            member = sg.group.getMember(userId);
            //see if the user has viewHidden permission at the group level too
            viewHidden = viewHidden || securityService.unlock("roster.viewHidden", sg.group.getReference());
        }
        if (member != null && !privacyManager
                .findHidden(sg.site.getReference(), new HashSet<String>(Arrays.asList(userId))).contains(userId)) {
            EntityUser eu = userEntityProvider.getUserById(userId);
            em = new EntityMember(member, sg.locationReference, eu);
        }
        return em;
    }

    /**
     * @param locationReference
     *            a site ref with an optional group ref (can look like this:
     *            /site/siteid/group/groupId)
     * @return the list of memberships for the given location and role
     */
    public List<EntityMember> getMembers(String locationReference) {
        ArrayList<EntityMember> l = new ArrayList<EntityMember>();
        Set<Member> members = null;
        SiteGroup sg;
        try {
            sg = findLocationByReference(locationReference);
        } catch (IllegalArgumentException e) {
            throw new EntityNotFoundException(
                    "Could not find the location based on the ref (" + locationReference + "): " + e,
                    locationReference);
        }
        isAllowedAccessMembers(sg.site, sg.group);
        boolean viewHidden = viewHidden = securityService.unlock("roster.viewHidden", sg.site.getReference());
        Set<String> hiddenUsers = new HashSet<String>();
        if (sg.group == null) {
            // site only
            members = sg.site.getMembers();
        } else {
            // group and site
            members = sg.group.getMembers();
            //see if user has the ability to view hidden at the group level as well
            viewHidden = viewHidden || securityService.unlock("roster.viewHidden", sg.group.getReference());
        }
        if (!siteService.allowViewRoster(sg.site.getId()) && !viewHidden) {
            //add hidden users to set so we can filter them out
            Set<String> memberIds = new HashSet<String>();
            for (Member member : members) {
                memberIds.add(member.getUserId());
            }
            hiddenUsers = privacyManager.findHidden(sg.site.getReference(), memberIds);
        }
        // filter out possible invalid/orphaned users (SAK-22396, SAK-17498, SAK-23863)
        for (Member member : members) {
            EntityUser eu = userEntityProvider.getUserById(member.getUserId());
            if (eu != null && !hiddenUsers.contains(member.getUserId())) {
                EntityMember em = new EntityMember(member, sg.locationReference, eu);
                l.add(em);
            }
        }
        return l;
    }

    /**
     * Find a site (and optionally group) by reference
     * 
     * @param locationReference
     * @return a Site and optional group
     * @throws IllegalArgumentException
     *             if they cannot be found for this ref
     */
    public SiteGroup findLocationByReference(String locationReference) {
        SiteGroup holder = new SiteGroup(locationReference);
        if (locationReference.contains("/group/")) {
            // group membership
            String groupId = EntityReference.getIdFromRefByKey(locationReference, "group");
            if (groupId == null || "".equals(groupId)) {
                throw new IllegalArgumentException(
                        "locationReferences for groups must be structured like this: /site/siteid/group/groupId or /group/groupId, could not find group in: "
                                + locationReference);
            }
            locationReference = "/group/" + groupId;
            Group group = siteService.findGroup(groupId);
            // an invalid group ID might be passed which results in a null here
            if (group == null) {
                throw new IllegalArgumentException("No group found for id: " + groupId);
            }
            Site site = group.getContainingSite();
            holder.locationReference = locationReference;
            holder.group = group;
            holder.site = site;
        } else if (locationReference.contains("/site/")) {
            // site membership
            String siteId = EntityReference.getIdFromRefByKey(locationReference, "site");
            Site site = getSiteById(siteId);
            holder.site = site;
        } else {
            throw new IllegalArgumentException("Do not know how to handle this location reference ("
                    + locationReference + "), only can handle site and group references");
        }
        if (holder.site == null) {
            throw new IllegalArgumentException(
                    "Could not find a site/group with the given reference: " + locationReference);
        }
        return holder;
    }

    /**
     * Look for a batch membership operation
     * 
     * @param params
     * @param userId
     * @return
     */
    protected String[] checkForBatch(Map<String, Object> params, String userId) {
        HashSet<String> userIds = new HashSet<String>();
        if (userId != null) {
            userIds.add(userId);
        }
        if (params != null) {
            List<String> batchUserIds = getListFromValue(params.get("userIds"));
            for (String batchUserId : batchUserIds) {
                String uid = userEntityProvider.findAndCheckUserId(batchUserId, null);
                if (uid != null) {
                    userIds.add(uid);
                }
            }

        }
        if (log.isDebugEnabled())
            log.debug("Received userIds=" + userIds);
        return userIds.toArray(new String[userIds.size()]);
    }

    protected List<String> getListFromValue(Object paramValue) {
        List<String> stringList = new ArrayList<String>();
        if (paramValue != null) {
            if (paramValue.getClass().isArray()) {
                stringList = Arrays.asList((String[]) paramValue);
            } else if (paramValue instanceof String) {
                stringList.add((String) paramValue);
            } else if (paramValue.getClass().isInstance(new ArrayList<String>())) {
                return (List<String>) paramValue;
            }
        }
        return stringList;
    }

    protected String makeRoleId(String currentRoleId, Site site) {
        String roleId = currentRoleId;
        if (roleId == null || "".equals(roleId)) {
            roleId = site.getJoinerRole();
        }
        return roleId;
    }

    /**
     * @param site
     * @param group
     */
    protected void saveGroupMembership(Site site, Group group) {
        try {
            siteService.saveGroupMembership(site);
        } catch (IdUnusedException e) {
            throw new IllegalArgumentException("Invalid site: " + site.getId() + ":" + e.getMessage(), e);
        } catch (PermissionException e) {
            throw new SecurityException("Current user (" + developerHelperService.getCurrentUserId()
                    + ") not allowed to update site group memberships in group: " + group.getId() + " :"
                    + e.getMessage() + ":" + e.getCause(), e);
        }
    }

    /**
     * @param site
     */
    protected void saveSiteMembership(Site site) {
        checkSiteSecurity(site.getId());
        try {
            siteService.saveSiteMembership(site);
        } catch (IdUnusedException e) {
            throw new IllegalArgumentException("Invalid site: " + site.getId() + ":" + e.getMessage(), e);
        } catch (PermissionException e) {
            throw new SecurityException("Current user (" + developerHelperService.getCurrentUserId()
                    + ") not allowed to update site memberships in site: " + site.getId() + " :" + e.getMessage()
                    + ":" + e.getCause(), e);
        }
    }

    protected Site getSiteById(String siteId) {
        Site site;
        try {
            site = siteService.getSite(siteId);
        } catch (IdUnusedException e) {
            throw new IllegalArgumentException("Cannot find site by siteId: " + siteId, e);
        }
        return site;
    }

    /**
     * @param site
     *            the site to check perms in
     * @return true if the current user can view this site
     * @throws SecurityException
     *             if not allowed
     */
    protected boolean isAllowedAccessMembers(Site site, Group g) {
        // check if the current user can access this
        String userReference = developerHelperService.getCurrentUserReference();
        if (userReference == null) {
            throw new SecurityException(
                    "Anonymous users may not view memberships in (" + site.getReference() + ")");
        } else {
            String siteId = site.getId();
            if (siteService.allowViewRoster(siteId)) {
                return true;
            } else if (g != null && Boolean.TRUE.toString()
                    .equals(g.getProperties().getProperty(Group.GROUP_PROP_VIEW_MEMBERS))) {
                return true;
            } else {
                throw new SecurityException("Memberships in this site (" + site.getReference()
                        + ") are not accessible for the current user: " + userReference);
            }
        }
    }

    /**
     * This contains the site and optionally group for a given reference
     * 
     * @author Aaron Zeckoski (azeckoski @ gmail.com)
     */
    public static class SiteGroup {

        public Site site;
        public Group group;
        public String locationReference;

        public SiteGroup(String locationReference) {
            this.locationReference = locationReference;
        }
    }

    /**
     * This adds the users to the group provided in the site provided
     * 
     * @param site
     *            The site which the group belongs
     * @param group
     *            The group to add the users to
     * @param userIds
     *            The list of Uuids to use
     * NOTE: not used?
     */
    protected void addUsersToGroup(Site site, Group group, List<String> userIds) {
        for (String user : userIds) {
            String userId = user.trim();
            Role role = site.getUserRole(userId);
            Member m = site.getMember(userId);

            if (group.getUserRole(userId) == null && role.getId() != null) {
                // Every user added via this EB is defined as non-provided
                group.addMember(userId, role.getId(), m != null ? m.isActive() : true, false);
            }
        }
    }

    /**
     * Only handle Site Info type groups.
     * 
     * @param group
     * @throws IllegalArgumentException
     *             if NOT a Site Info type group
     */
    private void checkGroupType(Group group) {
        if (group != null) {
            try {
                if (!group.getProperties().getBooleanProperty(GROUP_PROP_WSETUP_CREATED)) {
                    throw new IllegalArgumentException(
                            "This type of group (Section Info group) should not be edited by this entity provider. Only Site info groups are allowed.");
                }
            } catch (Exception e) {
                throw new IllegalArgumentException(
                        "This type of group (Section Info group) should not be edited by this entity provider. Only Site info groups are allowed.");
            }
        }
    }

    /**
     * SAK-20828 handle low hanging CSRF blocking
     * @param siteId the sakai site id
     * @throws SecurityException if this site cannot be updated via provider
     */
    private void checkSiteSecurity(String siteId) {
        if (!allowAdminSiteChanges && ADMIN_SITE_ID.equals(siteId)) {
            throw new SecurityException(
                    "Admin site membership changes are disabled for security protection against CSRF, you must use the sakai admin UI or enable changes in your sakai config file using "
                            + ADMIN_SITE_CHANGE_ALLOWED + "=true");
        }
    }

    public static class JsonUser {
        private String id = "";
        private String role = "";

        public JsonUser() {
        }

        public JsonUser(String id, String role) {
            this.id = id;
            this.role = role;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getRole() {
            return role;
        }

        public void setRole(String role) {
            this.role = role;
        }
    }

}