Implemented #17 and added role removal after deleting members.

Change-Id: I698a194170db7460bde3bbb9db6c20b0dc395975
diff --git a/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java b/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
index f32fb89..1c78ef0 100644
--- a/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
+++ b/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
@@ -84,11 +84,12 @@
     
     // User group and member 
     public static final int GROUP_MEMBER_EXISTS = 601;
-    public static final int GROUP_MEMBER_DELETED = 602;
-    public static final int GROUP_MEMBER_NOT_FOUND = 603;
-    public static final int INVITATION_EXPIRED = 604;
-    public static final int GROUP_NOT_FOUND = 605;
-    public static final int GROUP_DELETED = 606;
+    public static final int GROUP_MEMBER_INACTIVE = 602;
+    public static final int GROUP_MEMBER_DELETED = 603;
+    public static final int GROUP_MEMBER_NOT_FOUND = 604;
+    public static final int INVITATION_EXPIRED = 605;
+    public static final int GROUP_NOT_FOUND = 606;
+    public static final int GROUP_DELETED = 607;
     
 
 //    public static final int ARGUMENT_VALIDATION_FAILURE = 700;
diff --git a/full/Changes b/full/Changes
index 809ebc0..9e651d1 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
 version 0.60.1
-20/03/2018
+28/03/2018
 	- added admin-related SQL codes (margaretha)
 	- updated AdminDao (margaretha)
 	- added optional username query parameter to group list controller (margaretha) 
@@ -16,6 +16,9 @@
 	- added check for hidden groups in VC controller tests (margaretha)
 	- added search user-group controller (margaretha)
 	- removed createdBy from VirtualCorpusJson (margaretha)
+	- moved member role setting from the invitation phase to the after-subscription phase (margaretha)
+	- added member role removal after deleting members (margaretha)
+	- added add and delete member role controllers (margaretha)
 	
 version 0.60
 14/03/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index bee11e3..1d48b02 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -1,7 +1,9 @@
 package de.ids_mannheim.korap.dao;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.persistence.EntityManager;
 import javax.persistence.NoResultException;
@@ -65,7 +67,7 @@
         group.setCreatedBy(createdBy);
         entityManager.persist(group);
 
-        List<Role> roles = new ArrayList<Role>(2);
+        Set<Role> roles = new HashSet<Role>();
         roles.add(roleDao
                 .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId()));
         roles.add(roleDao
@@ -282,23 +284,22 @@
 
             ListJoin<UserGroup, UserGroupMember> members =
                     root.join(UserGroup_.members);
-            restrictions = criteriaBuilder.and(
-                    criteriaBuilder.equal(members.get(UserGroupMember_.userId),
-                            userId));
-            
-            if (status != null){
+            restrictions = criteriaBuilder.and(criteriaBuilder
+                    .equal(members.get(UserGroupMember_.userId), userId));
+
+            if (status != null) {
                 restrictions = criteriaBuilder.and(restrictions, criteriaBuilder
                         .equal(root.get(UserGroup_.status), status));
             }
         }
         else if (status != null) {
-                restrictions = criteriaBuilder
-                        .equal(root.get(UserGroup_.status), status);
-                
+            restrictions =
+                    criteriaBuilder.equal(root.get(UserGroup_.status), status);
+
         }
 
         query.select(root);
-        if (restrictions!=null){
+        if (restrictions != null) {
             query.where(restrictions);
         }
         Query q = entityManager.createQuery(query);
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index b4f2227..47fef03 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -1,5 +1,6 @@
 package de.ids_mannheim.korap.dao;
 
+import java.util.HashSet;
 import java.util.List;
 
 import javax.persistence.EntityManager;
@@ -43,16 +44,6 @@
         entityManager.persist(member);
     }
 
-    //    @Deprecated
-    //    public void addMembers (List<UserGroupMember> members)
-    //            throws KustvaktException {
-    //        ParameterChecker.checkObjectValue(members, "List<UserGroupMember>");
-    //
-    //        for (UserGroupMember member : members) {
-    //            addMember(member);
-    //        }
-    //    }
-
     public void updateMember (UserGroupMember member) throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "UserGroupMember");
         entityManager.merge(member);
@@ -70,9 +61,11 @@
         if (isSoftDelete) {
             member.setStatus(GroupMemberStatus.DELETED);
             member.setDeletedBy(deletedBy);
+            member.setRoles(new HashSet<>());
             entityManager.persist(member);
         }
         else {
+            member.setRoles(new HashSet<>());
             entityManager.remove(member);
         }
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java b/full/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
index 0ee2912..3978799 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
@@ -2,6 +2,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 import org.springframework.stereotype.Component;
 
@@ -11,6 +12,7 @@
 import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.UserGroupMember;
+import edu.emory.mathcs.backport.java.util.Collections;
 
 /** Manages conversion of  {@link UserGroup} objects to their data access objects (DTO), 
  * e.g. UserGroupDto. DTO structure defines controllers output, namely the structure of 
@@ -24,7 +26,7 @@
 
     public UserGroupDto createUserGroupDto (UserGroup group,
             List<UserGroupMember> members, GroupMemberStatus userMemberStatus,
-            List<Role> userRoles) {
+            Set<Role> roleSet) {
 
         UserGroupDto dto = new UserGroupDto();
         dto.setId(group.getId());
@@ -33,12 +35,8 @@
         dto.setOwner(group.getCreatedBy());
         dto.setUserMemberStatus(userMemberStatus);
 
-        if (userRoles != null) {
-            List<String> roles = new ArrayList<>(userRoles.size());
-            for (Role r : userRoles) {
-                roles.add(r.getName());
-            }
-            dto.setUserRoles(roles);
+        if (roleSet != null) {
+            dto.setUserRoles(convertRoleSetToStringList(roleSet));
         }
 
         if (members != null) {
@@ -49,12 +47,8 @@
                 UserGroupMemberDto memberDto = new UserGroupMemberDto();
                 memberDto.setUserId(member.getUserId());
                 memberDto.setStatus(member.getStatus());
-                List<String> memberRoles =
-                        new ArrayList<>(member.getRoles().size());
-                for (Role r : member.getRoles()) {
-                    memberRoles.add(r.getName());
-                }
-                memberDto.setRoles(memberRoles);
+                memberDto.setRoles(
+                        convertRoleSetToStringList(member.getRoles()));
                 memberDtos.add(memberDto);
             }
             dto.setMembers(memberDtos);
@@ -66,4 +60,12 @@
         return dto;
     }
 
+    private List<String> convertRoleSetToStringList (Set<Role> roleSet) {
+        List<String> roles = new ArrayList<>(roleSet.size());
+        for (Role r : roleSet) {
+            roles.add(r.getName());
+        }
+        Collections.sort(roles);
+        return roles;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/Role.java b/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
index 2058abf..1474316 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
@@ -26,7 +26,7 @@
 @Getter
 @Entity
 @Table(name = "role")
-public class Role {
+public class Role implements Comparable<Role>{
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private int id;
@@ -43,4 +43,15 @@
     public String toString () {
         return "id=" + id + ", name=" + name;
     }
+
+    @Override
+    public int compareTo (Role o) {
+        if (this.getId() > o.getId()){
+            return 1;
+        }
+        else if (this.getId() < o.getId()){
+            return -1;
+        }
+        return 0;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java b/full/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
index bcbd100..983535b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
@@ -1,7 +1,7 @@
 package de.ids_mannheim.korap.entity;
 
 import java.time.ZonedDateTime;
-import java.util.List;
+import java.util.Set;
 
 import javax.persistence.Column;
 import javax.persistence.Entity;
@@ -71,7 +71,7 @@
                     referencedColumnName = "id"),
             uniqueConstraints = @UniqueConstraint(
                     columnNames = { "group_member_id", "role_id" }))
-    private List<Role> roles;
+    private Set<Role> roles;
 
     @Override
     public String toString () {
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/MailService.java b/full/src/main/java/de/ids_mannheim/korap/service/MailService.java
index 556d7b7..37b216d 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/MailService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/MailService.java
@@ -18,8 +18,10 @@
 import org.springframework.stereotype.Service;
 
 import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
 import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.ParameterChecker;
 
 /** Manages mail related services, such as sending group member invitations 
  * per email.  
@@ -42,8 +44,12 @@
     private FullConfiguration config;
 
     public void sendMemberInvitationNotification (String inviteeName,
-            String groupName, String inviter) {
+            String groupName, String inviter) throws KustvaktException {
 
+        ParameterChecker.checkStringValue(inviteeName, "inviteeName");
+        ParameterChecker.checkStringValue(groupName, "groupName");
+        ParameterChecker.checkStringValue(inviter, "inviter");
+        
         MimeMessagePreparator preparator = new MimeMessagePreparator() {
 
             public void prepare (MimeMessage mimeMessage) throws Exception {
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java b/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
index e79db77..e8fc312 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -3,7 +3,10 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +59,7 @@
     @Autowired
     private MailService mailService;
 
-    private static List<Role> memberRoles;
+    private static Set<Role> memberRoles;
 
     /** Only users with {@link PredefinedRole#USER_GROUP_ADMIN} 
      * are allowed to see the members of the group.
@@ -153,16 +156,9 @@
         return groupAdmins;
     }
 
-    public List<UserGroupMember> retrieveUserGroupAdmins (UserGroup userGroup)
-            throws KustvaktException {
-        List<UserGroupMember> groupAdmins = groupMemberDao.retrieveMemberByRole(
-                userGroup.getId(), PredefinedRole.USER_GROUP_ADMIN.getId());
-        return groupAdmins;
-    }
-
     private void setMemberRoles () {
         if (memberRoles == null) {
-            memberRoles = new ArrayList<Role>(2);
+            memberRoles = new HashSet<Role>(2);
             memberRoles.add(roleDao.retrieveRoleById(
                     PredefinedRole.USER_GROUP_MEMBER.getId()));
             memberRoles.add(roleDao
@@ -199,9 +195,6 @@
                 UserGroupStatus.ACTIVE);
         UserGroup userGroup = userGroupDao.retrieveGroupById(groupId);
 
-        setMemberRoles();
-
-
         for (String memberId : groupJson.getMembers()) {
             if (memberId.equals(createdBy)) {
                 // skip owner, already added while creating group.
@@ -266,6 +259,18 @@
             String createdBy, GroupMemberStatus status)
             throws KustvaktException {
 
+        addGroupMember(username, userGroup, createdBy, status);
+
+        if (config.isMailEnabled()
+                && userGroup.getStatus() != UserGroupStatus.HIDDEN) {
+            mailService.sendMemberInvitationNotification(username,
+                    userGroup.getName(), createdBy);
+        }
+    }
+
+    public void addGroupMember (String username, UserGroup userGroup,
+            String createdBy, GroupMemberStatus status)
+            throws KustvaktException {
         int groupId = userGroup.getId();
         ParameterChecker.checkIntegerValue(groupId, "userGroupId");
 
@@ -279,21 +284,12 @@
                     username, existingStatus.name(), userGroup.getName());
         }
 
-        setMemberRoles();
-
         UserGroupMember member = new UserGroupMember();
         member.setCreatedBy(createdBy);
         member.setGroup(userGroup);
-        member.setRoles(memberRoles);
         member.setStatus(status);
         member.setUserId(username);
         groupMemberDao.addMember(member);
-
-        if (config.isMailEnabled()
-                && userGroup.getStatus() != UserGroupStatus.HIDDEN) {
-            mailService.sendMemberInvitationNotification(username,
-                    userGroup.getName(), createdBy);
-        }
     }
 
     private GroupMemberStatus memberExists (String username, int groupId,
@@ -314,7 +310,7 @@
         }
         else if (existingStatus.equals(GroupMemberStatus.DELETED)) {
             // hard delete, not customizable
-            deleteMember(username, groupId, "system", false);
+            doDeleteMember(username, groupId, "system", false);
         }
 
         return null;
@@ -348,8 +344,11 @@
 
     private boolean isUserGroupAdmin (String username, UserGroup userGroup)
             throws KustvaktException {
+
         List<UserGroupMember> userGroupAdmins =
-                retrieveUserGroupAdmins(userGroup);
+                groupMemberDao.retrieveMemberByRole(userGroup.getId(),
+                        PredefinedRole.USER_GROUP_ADMIN.getId());
+
         for (UserGroupMember admin : userGroupAdmins) {
             if (username.equals(admin.getUserId())) {
                 return true;
@@ -359,7 +358,7 @@
     }
 
     /** Updates the {@link GroupMemberStatus} of a pending member 
-     * to {@link GroupMemberStatus#ACTIVE}.
+     * to {@link GroupMemberStatus#ACTIVE} and add default member roles.
      * 
      * @param groupId groupId
      * @param username the username of the group member
@@ -403,6 +402,8 @@
 
             if (expiration.isAfter(now)) {
                 member.setStatus(GroupMemberStatus.ACTIVE);
+                setMemberRoles();
+                member.setRoles(memberRoles);
                 groupMemberDao.updateMember(member);
             }
             else {
@@ -442,7 +443,7 @@
                 || isUserGroupAdmin(deletedBy, userGroup)
                 || adminDao.isAdmin(deletedBy)) {
             // soft delete
-            deleteMember(memberId, groupId, deletedBy,
+            doDeleteMember(memberId, groupId, deletedBy,
                     config.isSoftDeleteGroupMember());
         }
         else {
@@ -461,7 +462,7 @@
      * permanently, false otherwise
      * @throws KustvaktException
      */
-    private void deleteMember (String username, int groupId, String deletedBy,
+    private void doDeleteMember (String username, int groupId, String deletedBy,
             boolean isSoftDelete) throws KustvaktException {
 
         UserGroup group = userGroupDao.retrieveGroupById(groupId);
@@ -494,5 +495,75 @@
 
     }
 
+    public void addMemberRoles (String username, int groupId,
+            String memberUsername, List<Integer> roleIds) throws KustvaktException {
 
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupById(groupId, true);
+        UserGroupStatus groupStatus = userGroup.getStatus();
+        if (groupStatus == UserGroupStatus.DELETED){
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Usergroup has been deleted."); 
+        }
+        else if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member =
+                    groupMemberDao.retrieveMemberById(memberUsername, groupId);
+
+            if (!member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+                throw new KustvaktException(StatusCodes.GROUP_MEMBER_INACTIVE,
+                        memberUsername + " has status " + member.getStatus(),
+                        memberUsername, member.getStatus().name());
+            }
+
+            Set<Role> roles = member.getRoles();
+            for (int i = 0; i < roleIds.size(); i++) {
+                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+            }
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public void deleteMemberRoles (String username, int groupId,
+            String memberUsername, List<Integer> roleIds) throws KustvaktException {
+
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupById(groupId, true);
+
+        if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member =
+                    groupMemberDao.retrieveMemberById(memberUsername, groupId);
+
+            Set<Role> roles = member.getRoles();
+            Iterator<Role> i = roles.iterator();
+            while (i.hasNext()){
+                if (roleIds.contains(i.next().getId())){
+                    i.remove();
+                }
+            }
+
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
index 0643baa..5835d92 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
@@ -423,8 +423,9 @@
                 UserGroup userGroup =
                         userGroupService.retrieveHiddenUserGroupByVC(vcId);
                 try {
-                    userGroupService.inviteGroupMember(username, userGroup,
+                    userGroupService.addGroupMember(username, userGroup,
                             "system", GroupMemberStatus.ACTIVE);
+                    // member roles has not been set (not necessary)
                 }
                 catch (KustvaktException e) {
                     // member exists
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
index 6157b24..ecc00bb 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -23,7 +23,6 @@
 
 import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dto.UserGroupDto;
-import de.ids_mannheim.korap.dto.VirtualCorpusDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.service.UserGroupService;
 import de.ids_mannheim.korap.user.TokenContext;
@@ -81,7 +80,7 @@
         }
     }
 
-    
+
     /** Lists user-groups for system-admin purposes. If username parameter 
      *  is not specified, list user-groups of all users. If status is not
      *  specified, list user-groups of all statuses.
@@ -110,14 +109,14 @@
         }
     }
 
-    /** Searches for a specific user-group for system admins.
+    /** Retrieves a specific user-group for system admins.
      * 
      * @param securityContext
      * @param groupId group id
      * @return a user-group
      */
     @GET
-    @Path("search/{groupId}")
+    @Path("{groupId}")
     public Response searchUserGroup (@Context SecurityContext securityContext,
             @PathParam("groupId") int groupId) {
         String result;
@@ -133,7 +132,7 @@
         }
         return Response.ok(result).build();
     }
-    
+
     /** Creates a user group where the user in token context is the 
      * group owner, and assigns the listed group members with status 
      * GroupMemberStatus.PENDING. 
@@ -169,7 +168,8 @@
         }
     }
 
-    /** Only group owner and system admins can delete groups. 
+    /** Deletes a user-group specified by the group id. Only group owner 
+     *  and system admins can delete groups. 
      * 
      * @param securityContext
      * @param groupId
@@ -190,7 +190,7 @@
         }
     }
 
-    /** Group owner cannot be deleted.
+    /** Deletes a user-group member. Group owner cannot be deleted.
      * 
      * @param securityContext
      * @param memberId a username of a group member
@@ -214,6 +214,12 @@
         }
     }
 
+    /** Invites group members to join a user-group specified in the JSON object.
+     * @param securityContext
+     * @param group UserGroupJson containing groupId and usernames to be invited
+     * as members 
+     * @return if successful, HTTP response status OK
+     */
     @POST
     @Path("member/invite")
     @Consumes(MediaType.APPLICATION_JSON)
@@ -230,6 +236,69 @@
         }
     }
 
+    /** Adds roles of an active member of a user-group. Only user-group admins
+     * and system admins are allowed.
+     * 
+     * @param securityContext
+     * @param groupId a group id
+     * @param memberUsername the username of a group member
+     * @param roleIds list of role ids
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("member/role/add")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response addMemberRoles (@Context SecurityContext securityContext,
+            @FormParam("groupId") int groupId,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleIds") List<Integer> roleIds) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            service.addMemberRoles(context.getUsername(), groupId,
+                    memberUsername, roleIds);
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /** Deletes roles of a member of a user-group. Only user-group admins
+     * and system admins are allowed.
+     * 
+     * @param securityContext
+     * @param groupId a group id
+     * @param memberUsername the username of a group member
+     * @param roleIds list of role ids
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("member/role/delete")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response deleteMemberRoles (@Context SecurityContext securityContext,
+            @FormParam("groupId") int groupId,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleIds") List<Integer> roleIds) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            service.deleteMemberRoles(context.getUsername(), groupId,
+                    memberUsername, roleIds);
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
+
+    /** Handles requests to accept membership invitation. Only invited users 
+     * can subscribe to the corresponding user-group. 
+     * 
+     * @param securityContext
+     * @param groupId a group id
+     * @return if successful, HTTP response status OK
+     */
     @POST
     @Path("subscribe")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -246,6 +315,15 @@
         }
     }
 
+    /** Handles requests to reject membership invitation. A member can only 
+     * unsubscribe him/herself from a group. 
+     * 
+     * Implemented identical to delete group member.
+     * 
+     * @param securityContext
+     * @param groupId
+     * @return if successful, HTTP response status OK
+     */
     @POST
     @Path("unsubscribe")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
diff --git a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
index 4bb8ba0..11f326f 100644
--- a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
@@ -2,8 +2,14 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
+
+import javax.validation.constraints.AssertTrue;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -25,6 +31,7 @@
 import de.ids_mannheim.korap.entity.VirtualCorpus;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.user.User.CorpusAccess;
+import edu.emory.mathcs.backport.java.util.Collections;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration("classpath:test-config.xml")
@@ -41,7 +48,7 @@
 
     @Rule
     public ExpectedException thrown = ExpectedException.none();
-    
+
 
     @Test
     public void createDeleteNewUserGroup () throws KustvaktException {
@@ -69,15 +76,18 @@
         // member roles
         List<Role> roles = roleDao.retrieveRoleByGroupMemberId(m.getId());
         assertEquals(2, roles.size());
-        assertEquals(PredefinedRole.USER_GROUP_ADMIN.getId(), roles.get(0).getId());
-        assertEquals(PredefinedRole.VC_ACCESS_ADMIN.getId(), roles.get(1).getId());
+        assertEquals(PredefinedRole.USER_GROUP_ADMIN.getId(),
+                roles.get(0).getId());
+        assertEquals(PredefinedRole.VC_ACCESS_ADMIN.getId(),
+                roles.get(1).getId());
 
         //retrieve VC by group
         List<VirtualCorpus> vc = virtualCorpusDao.retrieveVCByGroup(groupId);
         assertEquals(0, vc.size());
 
         // soft delete group
-        userGroupDao.deleteGroup(groupId, createdBy, config.isSoftDeleteGroup());
+        userGroupDao.deleteGroup(groupId, createdBy,
+                config.isSoftDeleteGroup());
         group = userGroupDao.retrieveGroupById(groupId);
         assertEquals(UserGroupStatus.DELETED, group.getStatus());
 
@@ -95,10 +105,16 @@
         assertEquals(4, members.size());
 
         UserGroupMember m = members.get(1);
-        List<Role> roles = m.getRoles();
+        Set<Role> roles = m.getRoles();
         assertEquals(2, roles.size());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.getId(), roles.get(0).getId());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.getId(), roles.get(1).getId());
+
+        List<Role> sortedRoles = new ArrayList<>(roles);
+        Collections.sort(sortedRoles);
+
+        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
+                sortedRoles.get(0).getName());
+        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
+                sortedRoles.get(1).getName());
     }
 
     @Test
@@ -132,8 +148,9 @@
         UserGroup group = userGroupDao.retrieveGroupById(groupId);
         String createdBy = "dory";
         String name = "dory new vc";
-        int id = virtualCorpusDao.createVirtualCorpus(name, VirtualCorpusType.PROJECT,
-                CorpusAccess.PUB, "corpusSigle=WPD15", "", "", "", createdBy);
+        int id = virtualCorpusDao.createVirtualCorpus(name,
+                VirtualCorpusType.PROJECT, CorpusAccess.PUB,
+                "corpusSigle=WPD15", "", "", "", createdBy);
 
         VirtualCorpus virtualCorpus = virtualCorpusDao.retrieveVCById(id);
         userGroupDao.addVCToGroup(virtualCorpus, createdBy,
@@ -148,7 +165,7 @@
 
         vc = virtualCorpusDao.retrieveVCByGroup(groupId);
         assertEquals(1, vc.size());
-        
+
         // delete vc
         virtualCorpusDao.deleteVirtualCorpus(virtualCorpus);
     }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java
index a80397c..d1cbfa6 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java
@@ -3,6 +3,8 @@
 import static org.junit.Assert.assertEquals;
 
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
 
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -12,6 +14,7 @@
 import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
 import com.sun.jersey.api.client.ClientResponse.Status;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
 import com.sun.jersey.api.client.UniformInterfaceException;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
@@ -59,7 +62,7 @@
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.getEntity(String.class);
-//        System.out.println(entity);
+        //        System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(3, node.size());
     }
@@ -77,12 +80,12 @@
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.getEntity(String.class);
-//        System.out.println(entity);
+        //        System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(2, node.size());
     }
-    
-    
+
+
     // same as list user-groups of the admin
     @Test
     public void testListWithoutUsername () throws UniformInterfaceException,
@@ -160,11 +163,114 @@
         assertEquals("admin test group", node.get("name").asText());
 
         String groupId = node.get("id").asText();
+        testMemberRole("marlin", groupId);
         testInviteMember(groupId);
         testDeleteMember(groupId);
         testDeleteGroup(groupId);
     }
 
+    private void testMemberRole (String memberUsername, String groupId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        // accept invitation
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", groupId);
+
+        ClientResponse response = resource().path("group").path("subscribe")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("marlin",
+                                "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        testAddMemberRoles(groupId, memberUsername);
+        testDeleteMemberRoles(groupId, memberUsername);
+    }
+
+    private void testAddMemberRoles (String groupId, String memberUsername)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> map = new MultivaluedHashMap<>();
+        map.add("groupId", groupId.toString());
+        map.add("memberUsername", memberUsername);
+        map.add("roleIds", "1"); // USER_GROUP_ADMIN
+        map.add("roleIds", "2"); // USER_GROUP_MEMBER
+
+        ClientResponse response =
+                resource().path("group").path("member").path("role").path("add")
+                        .type(MediaType.APPLICATION_FORM_URLENCODED)
+                        .header(Attributes.AUTHORIZATION,
+                                handler.createBasicAuthorizationHeaderValue(
+                                        adminUsername, "password"))
+                        .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                        .entity(map).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = retrieveGroup(groupId).at("/members");
+        JsonNode member;
+        for (int i = 0; i < node.size(); i++) {
+            member = node.get(i);
+            if (member.at("/userId").asText().equals(memberUsername)) {
+                assertEquals(3, member.at("/roles").size());
+                assertEquals(PredefinedRole.USER_GROUP_ADMIN.name(),
+                        member.at("/roles/0").asText());
+                break;
+            }
+        }
+    }
+
+    private void testDeleteMemberRoles (String groupId, String memberUsername)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> map = new MultivaluedHashMap<>();
+        map.add("groupId", groupId.toString());
+        map.add("memberUsername", memberUsername);
+        map.add("roleIds", "1"); // USER_GROUP_ADMIN
+
+        ClientResponse response = resource().path("group").path("member")
+                .path("role").path("delete")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(
+                                adminUsername, "password"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(map)
+                .post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = retrieveGroup(groupId).at("/members");
+        JsonNode member;
+        for (int i = 0; i < node.size(); i++) {
+            member = node.get(i);
+            if (member.at("/userId").asText().equals(memberUsername)) {
+                assertEquals(2, member.at("/roles").size());
+                break;
+            }
+        }
+    }
+
+    private JsonNode retrieveGroup (String groupId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = resource().path("group").path(groupId)
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(
+                                adminUsername, "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
     private void testDeleteGroup (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
@@ -233,10 +339,7 @@
         assertEquals("darla", node.at("/members/3/userId").asText());
         assertEquals(GroupMemberStatus.PENDING.name(),
                 node.at("/members/3/status").asText());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                node.at("/members/3/roles/0").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                node.at("/members/3/roles/1").asText());
+        assertEquals(0, node.at("/members/3/roles").size());
     }
 
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
index 89d7ecd..373d194 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
@@ -151,7 +151,7 @@
                 .get(ClientResponse.class);
 
         String entity = response.getEntity(String.class);
-        //        System.out.println(entity);
+//        System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(1, node.size());
         node = node.get(0);
@@ -171,11 +171,7 @@
         assertEquals("marlin", node.at("/members/1/userId").asText());
         assertEquals(GroupMemberStatus.PENDING.name(),
                 node.at("/members/1/status").asText());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                node.at("/members/1/roles/0").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                node.at("/members/1/roles/1").asText());
-
+        assertEquals(0, node.at("/members/1/roles").size());
 
         testInviteMember(groupId);
 
@@ -212,10 +208,6 @@
         JsonNode node = JsonUtils.readTree(entity);
         node = node.get(0);
         assertEquals(3, node.get("members").size());
-        assertEquals("nemo", node.at("/members/1/userId").asText());
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                node.at("/members/1/status").asText());
-
     }
 
     private void testDeleteMemberUnauthorized (String groupId)
@@ -316,9 +308,9 @@
         assertEquals(groupId, node.at("/0/id").asText());
 
         // check group members
-        for (int i=0; i< node.at("/0/members").size(); i++){
+        for (int i = 0; i < node.at("/0/members").size(); i++) {
             assertEquals(GroupMemberStatus.DELETED.name(),
-                    node.at("/0/members/"+i+"/status").asText());    
+                    node.at("/0/members/" + i + "/status").asText());
         }
     }
 
@@ -424,10 +416,7 @@
         assertEquals("darla", node.at("/members/3/userId").asText());
         assertEquals(GroupMemberStatus.PENDING.name(),
                 node.at("/members/3/status").asText());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                node.at("/members/3/roles/0").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                node.at("/members/3/roles/1").asText());
+        assertEquals(0, node.at("/members/3/roles").size());
     }
 
     private void testInviteDeletedMember () throws UniformInterfaceException,
@@ -602,7 +591,7 @@
 
         // retrieve marlin group
         JsonNode node = retrieveUserGroups("marlin");
-        //        System.out.println(node);
+        // System.out.println(node);
         assertEquals(2, node.size());
 
         JsonNode group = node.get(1);
@@ -613,9 +602,15 @@
         assertEquals(0, group.at("/members").size());
         assertEquals(GroupMemberStatus.ACTIVE.name(),
                 group.at("/userMemberStatus").asText());
+        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
+                group.at("/userRoles/0").asText());
+        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
+                group.at("/userRoles/1").asText());
+
 
         // unsubscribe marlin from dory group
         testUnsubscribeActiveMember(form);
+        checkGroupMemberRole("2", "marlin");
 
         // invite marlin to dory group to set back the GroupMemberStatus.PENDING
         testInviteDeletedMember();
@@ -748,6 +743,29 @@
         assertEquals(1, node.size());
     }
 
+    private void checkGroupMemberRole (String groupId, String deletedMemberName)
+            throws KustvaktException {
+        ClientResponse response = resource().path("group").path(groupId)
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(admin,
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity).at("/members");
+        JsonNode member;
+        for (int i = 0; i < node.size(); i++) {
+            member = node.get(i);
+            if (deletedMemberName.equals(member.at("/userId").asText())) {
+                assertEquals(0, node.at("/roles").size());
+                break;
+            }
+        }
+    }
+
     @Test
     public void testUnsubscribeDeletedMember ()
             throws UniformInterfaceException, ClientHandlerException,
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index b61c972..7ef7361 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -362,7 +362,8 @@
 
         //EM: check if the hidden groups are deleted as well
         node = testCheckHiddenGroup(groupId);
-        assertEquals(605, node.at("/errors/0/0").asInt());
+        assertEquals(StatusCodes.GROUP_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
         assertEquals("Group with id 5 is not found",
                 node.at("/errors/0/1").asText());
     }
@@ -370,13 +371,12 @@
     private JsonNode testCheckHiddenGroup (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response =
-                resource().path("group").path("search").path(groupId)
-                        .header(Attributes.AUTHORIZATION,
-                                handler.createBasicAuthorizationHeaderValue(
-                                        "admin", "pass"))
-                        .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                        .get(ClientResponse.class);
+        ClientResponse response = resource().path("group").path(groupId)
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("admin",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
 
         String entity = response.getEntity(String.class);
         return JsonUtils.readTree(entity);