diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml
index c37d90e2fd..bf3bc658ae 100644
--- a/config/alfresco/public-rest-context.xml
+++ b/config/alfresco/public-rest-context.xml
@@ -1356,4 +1356,8 @@
+
+
+
+
diff --git a/source/java/org/alfresco/rest/api/Groups.java b/source/java/org/alfresco/rest/api/Groups.java
index 5b829e246e..96360ae70b 100644
--- a/source/java/org/alfresco/rest/api/Groups.java
+++ b/source/java/org/alfresco/rest/api/Groups.java
@@ -26,6 +26,7 @@
package org.alfresco.rest.api;
import org.alfresco.rest.api.model.Group;
+import org.alfresco.rest.api.model.GroupMember;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
@@ -41,6 +42,9 @@ public interface Groups
String PARAM_INCLUDE_PARENT_IDS = "parentIds";
String PARAM_INCLUDE_ZONES = "zones";
String PARAM_IS_ROOT = "isRoot";
+ String PARAM_MEMBER_TYPE = "memberType";
+ String PARAM_MEMBER_TYPE_GROUP = "GROUP";
+ String PARAM_MEMBER_TYPE_PERSON = "PERSON";
/**
* Gets a list of groups.
@@ -53,4 +57,15 @@ public interface Groups
*/
CollectionWithPagingInfo getGroups(Parameters parameters);
+ /**
+ * Gets a list of groups.
+ *
+ * @param groupId the identifier of a group.
+ * @param parameters the {@link Parameters} object to get the parameters passed into the request
+ * including:
+ * - filter, sort & paging params (where, orderBy, skipCount, maxItems)
+ * - incFiles, incFolders (both true by default)
+ * @return a paged list of {@code org.alfresco.rest.api.model.GroupMember} objects
+ */
+ CollectionWithPagingInfo getGroupMembers(String groupId, Parameters parameters);
}
diff --git a/source/java/org/alfresco/rest/api/groups/GroupMembersRelation.java b/source/java/org/alfresco/rest/api/groups/GroupMembersRelation.java
new file mode 100644
index 0000000000..d989628e59
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/groups/GroupMembersRelation.java
@@ -0,0 +1,64 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.groups;
+
+import org.alfresco.rest.api.Groups;
+import org.alfresco.rest.api.model.GroupMember;
+import org.alfresco.rest.framework.WebApiDescription;
+import org.alfresco.rest.framework.resource.RelationshipResource;
+import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
+import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
+import org.alfresco.rest.framework.resource.parameters.Parameters;
+import org.alfresco.util.ParameterCheck;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+
+ * @author cturlica
+ */
+@RelationshipResource(name = "members", entityResource = GroupsEntityResource.class, title = "Group Members")
+public class GroupMembersRelation implements RelationshipResourceAction.Read, InitializingBean
+{
+ private Groups groups;
+
+ public void setGroups(Groups groups)
+ {
+ this.groups = groups;
+ }
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ ParameterCheck.mandatory("groups", this.groups);
+ }
+
+ @Override
+ @WebApiDescription(title="A paged list of all the members of the group 'groupId'.")
+ public CollectionWithPagingInfo readAll(String groupId, Parameters params)
+ {
+ return groups.getGroupMembers(groupId, params);
+ }
+}
\ No newline at end of file
diff --git a/source/java/org/alfresco/rest/api/impl/GroupsImpl.java b/source/java/org/alfresco/rest/api/impl/GroupsImpl.java
index 67b1404fd2..52461d90d1 100644
--- a/source/java/org/alfresco/rest/api/impl/GroupsImpl.java
+++ b/source/java/org/alfresco/rest/api/impl/GroupsImpl.java
@@ -47,6 +47,7 @@ import org.alfresco.repo.security.authority.UnknownAuthorityException;
import org.alfresco.rest.antlr.WhereClauseParser;
import org.alfresco.rest.api.Groups;
import org.alfresco.rest.api.model.Group;
+import org.alfresco.rest.api.model.GroupMember;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
@@ -85,6 +86,8 @@ public class GroupsImpl implements Groups
// List groups filtering (via where clause)
private final static Set LIST_GROUPS_EQUALS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] { PARAM_IS_ROOT }));
+ private final static Set LIST_GROUP_MEMBERS_QUERY_PROPERTIES = new HashSet<>(Arrays.asList(new String[] { PARAM_MEMBER_TYPE }));
+
protected AuthorityService authorityService;
public AuthorityService getAuthorityService()
@@ -355,11 +358,11 @@ public class GroupsImpl implements Groups
if (v == null)
{
// Get the value from the group
- if (PARAM_DISPLAY_NAME.equals(sortBy))
+ if (DISPLAY_NAME.equals(sortBy))
{
v = g.getAuthorityDisplayName();
}
- else if (PARAM_ID.equals(sortBy))
+ else if (SHORT_NAME.equals(sortBy))
{
v = g.getAuthorityName();
}
@@ -377,4 +380,131 @@ public class GroupsImpl implements Groups
return v;
}
}
+
+ public CollectionWithPagingInfo getGroupMembers(String groupId, final Parameters parameters)
+ {
+ validateGroupId(groupId);
+
+ Paging paging = parameters.getPaging();
+
+ // Retrieve sort column. This is limited for now to sort column due to
+ // v0 api implementation. Should be improved in the future.
+ Pair sortProp = getGroupsSortProp(parameters);
+
+ AuthorityType authorityType = null;
+
+ // Parse where clause properties.
+ Query q = parameters.getQuery();
+ if (q != null)
+ {
+ MapBasedQueryWalkerOrSupported propertyWalker = new MapBasedQueryWalkerOrSupported(LIST_GROUP_MEMBERS_QUERY_PROPERTIES, null);
+ QueryHelper.walk(q, propertyWalker);
+
+ String memberTypeStr = propertyWalker.getProperty(PARAM_MEMBER_TYPE, WhereClauseParser.EQUALS, String.class);
+
+ if (memberTypeStr != null && !memberTypeStr.isEmpty())
+ {
+ switch (memberTypeStr)
+ {
+ case PARAM_MEMBER_TYPE_GROUP:
+ authorityType = AuthorityType.GROUP;
+ break;
+ case PARAM_MEMBER_TYPE_PERSON:
+ authorityType = AuthorityType.USER;
+ break;
+ default:
+ throw new InvalidArgumentException("MemberType is invalid (expected eg. GROUP, PERSON)");
+ }
+ }
+ }
+
+ PagingResults pagingResult = getAuthoritiesInfo(authorityType, groupId, sortProp, paging);
+
+ // Create response.
+ final List page = pagingResult.getPage();
+ int totalItems = pagingResult.getTotalResultCount().getFirst();
+ List groupMembers = new AbstractList()
+ {
+ @Override
+ public GroupMember get(int index)
+ {
+ AuthorityInfo authorityInfo = page.get(index);
+ return getGroupMember(authorityInfo);
+ }
+
+ @Override
+ public int size()
+ {
+ return page.size();
+ }
+ };
+
+ return CollectionWithPagingInfo.asPaged(paging, groupMembers, pagingResult.hasMoreItems(), totalItems);
+ }
+
+ private PagingResults getAuthoritiesInfo(AuthorityType authorityType, String groupId, Pair sortProp, Paging paging)
+ {
+ Set authorities;
+ try
+ {
+ authorities = authorityService.findAuthorities(authorityType, groupId, true, null, null);
+ }
+ catch (UnknownAuthorityException e)
+ {
+ authorities = Collections.emptySet();
+ }
+
+ List authorityInfoList = new ArrayList<>(authorities.size());
+ authorityInfoList.addAll(authorities.stream().map(this::getAuthorityInfo).collect(Collectors.toList()));
+
+ // Post process sorting - this should be moved to service
+ // layer. It is done here because sorting is not supported at
+ // service layer.
+ AuthorityInfoComparator authorityComparator = new AuthorityInfoComparator(sortProp.getFirst(), sortProp.getSecond());
+ Collections.sort(authorityInfoList, authorityComparator);
+
+ // Post process paging - this should be moved to service layer.
+ return Util.wrapPagingResults(paging, authorityInfoList);
+ }
+
+ private GroupMember getGroupMember(AuthorityInfo authorityInfo)
+ {
+ if (authorityInfo == null)
+ {
+ return null;
+ }
+
+ GroupMember groupMember = new GroupMember();
+ groupMember.setId(authorityInfo.getAuthorityName());
+ groupMember.setDisplayName(authorityInfo.getAuthorityDisplayName());
+
+ String memberType = null;
+ AuthorityType authorityType = AuthorityType.getAuthorityType(authorityInfo.getAuthorityName());
+ switch (authorityType)
+ {
+ case GROUP:
+ memberType = PARAM_MEMBER_TYPE_GROUP;
+ break;
+ case USER:
+ memberType = PARAM_MEMBER_TYPE_PERSON;
+ break;
+ default:
+ }
+ groupMember.setMemberType(memberType);
+
+ return groupMember;
+ }
+
+ private void validateGroupId(String groupId)
+ {
+ if (groupId == null || groupId.isEmpty())
+ {
+ throw new InvalidArgumentException("groupId is null or empty");
+ }
+
+ if (!authorityService.authorityExists(groupId))
+ {
+ throw new EntityNotFoundException(groupId);
+ }
+ }
}
diff --git a/source/java/org/alfresco/rest/api/model/GroupMember.java b/source/java/org/alfresco/rest/api/model/GroupMember.java
new file mode 100644
index 0000000000..b3931d5591
--- /dev/null
+++ b/source/java/org/alfresco/rest/api/model/GroupMember.java
@@ -0,0 +1,116 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.model;
+
+import org.alfresco.rest.framework.resource.UniqueId;
+
+/**
+ * Represents a group member.
+ *
+ * @author cturlica
+ *
+ */
+public class GroupMember implements Comparable
+{
+
+ private String id; // group id (aka authority name)
+ private String displayName;
+ private String memberType;
+
+ @UniqueId
+ public String getId()
+ {
+ return id;
+ }
+
+ public void setId(String id)
+ {
+ this.id = id;
+ }
+
+ public String getDisplayName()
+ {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName)
+ {
+ this.displayName = displayName;
+ }
+
+ public String getMemberType()
+ {
+ return memberType;
+ }
+
+ public void setMemberType(String memberType)
+ {
+ this.memberType = memberType;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+
+ if (obj == null)
+ {
+ return false;
+ }
+
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+
+ GroupMember other = (GroupMember) obj;
+ return id.equals(other.id);
+ }
+
+ @Override
+ public int compareTo(GroupMember group)
+ {
+ return id.compareTo(group.getId());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "GroupMember [id=" + id + ", displayName=" + displayName + ", memberType=" + memberType + "]";
+ }
+}
\ No newline at end of file
diff --git a/source/test-java/org/alfresco/rest/api/tests/GroupsTest.java b/source/test-java/org/alfresco/rest/api/tests/GroupsTest.java
index 815af94f11..97a6eca7ea 100644
--- a/source/test-java/org/alfresco/rest/api/tests/GroupsTest.java
+++ b/source/test-java/org/alfresco/rest/api/tests/GroupsTest.java
@@ -44,6 +44,7 @@ import org.alfresco.rest.api.tests.client.PublicApiClient.Groups;
import org.alfresco.rest.api.tests.client.PublicApiClient.ListResponse;
import org.alfresco.rest.api.tests.client.PublicApiClient.Paging;
import org.alfresco.rest.api.tests.client.data.Group;
+import org.alfresco.rest.api.tests.client.data.GroupMember;
import org.alfresco.rest.framework.resource.parameters.SortColumn;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
@@ -64,6 +65,8 @@ public class GroupsTest extends AbstractSingleNetworkSiteTest
private String rootGroupName = null;
private Group groupA = null;
private Group groupB = null;
+ private GroupMember groupMemberA = null;
+ private GroupMember groupMemberB = null;
@Before
public void setup() throws Exception
@@ -351,6 +354,12 @@ public class GroupsTest extends AbstractSingleNetworkSiteTest
groupB = new Group();
groupB.setId(groupBAuthorityName);
+
+ groupMemberA = new GroupMember();
+ groupMemberA.setId(groupAAuthorityName);
+
+ groupMemberB = new GroupMember();
+ groupMemberB.setId(groupBAuthorityName);
}
}
@@ -377,4 +386,182 @@ public class GroupsTest extends AbstractSingleNetworkSiteTest
assertNull(group.getParentIds());
assertNull(group.getZones());
}
+
+ private ListResponse getGroupMembers(String groupId, final PublicApiClient.Paging paging, Map otherParams, String errorMessage, int expectedStatus) throws Exception
+ {
+ final Groups groupsProxy = publicApiClient.groups();
+ return groupsProxy.getGroupMembers(groupId, createParams(paging, otherParams), errorMessage, expectedStatus);
+ }
+
+ private ListResponse getGroupMembers(String groupId, final PublicApiClient.Paging paging, Map otherParams) throws Exception
+ {
+ return getGroupMembers(groupId, paging, otherParams, "Failed to get group members", HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void testGetGroupMembers() throws Exception
+ {
+ try
+ {
+ createAuthorityContext(user1);
+
+ setRequestContext(user1);
+
+ testGetGroupMembersByGroupId();
+ testGetGroupMembersSorting();
+ testGetGroupMembersSkipPaging();
+ testGetGroupsByMemberType();
+ }
+ finally
+ {
+ clearAuthorityContext();
+ }
+ }
+
+ private void testGetGroupMembersByGroupId() throws Exception
+ {
+ Paging paging = getPaging(0, 4);
+
+ getGroupMembers(null, paging, null, "", HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ getGroupMembers("", paging, null, "", HttpServletResponse.SC_BAD_REQUEST);
+ getGroupMembers("invalidGroupId", paging, null, "", HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ private void testGetGroupMembersSorting() throws Exception
+ {
+ // orderBy=sortColumn should be the same to orderBy=sortColumn ASC
+ {
+ // paging
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+
+ Map otherParams = new HashMap<>();
+
+ // Default order.
+ addOrderBy(otherParams, org.alfresco.rest.api.Groups.PARAM_DISPLAY_NAME, null);
+
+ ListResponse resp = getGroupMembers(rootGroupName, paging, otherParams);
+ List groupMembers = resp.getList();
+ assertTrue("group members order not valid", groupMembers.indexOf(groupMemberA) < groupMembers.indexOf(groupMemberB));
+
+ // Ascending order
+ addOrderBy(otherParams, org.alfresco.rest.api.Groups.PARAM_DISPLAY_NAME, true);
+
+ ListResponse respOrderAsc = getGroupMembers(rootGroupName, paging, otherParams);
+
+ checkList(respOrderAsc.getList(), resp.getPaging(), resp);
+ }
+
+ // Sort by displayName.
+ {
+ // paging
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+
+ Map otherParams = new HashMap<>();
+
+ // Default order.
+ addOrderBy(otherParams, org.alfresco.rest.api.Groups.PARAM_DISPLAY_NAME, true);
+
+ ListResponse resp = getGroupMembers(rootGroupName, paging, otherParams);
+ List groupMembers = resp.getList();
+ assertTrue("group members order not valid", groupMembers.indexOf(groupMemberA) < groupMembers.indexOf(groupMemberB));
+ }
+
+ // Sort by id.
+ {
+ // paging
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+
+ Map otherParams = new HashMap<>();
+ addOrderBy(otherParams, org.alfresco.rest.api.Groups.PARAM_ID, false);
+
+ // list sites
+ ListResponse resp = getGroupMembers(rootGroupName, paging, otherParams);
+
+ List groupMembers = resp.getList();
+ assertTrue("group members order not valid", groupMembers.indexOf(groupMemberB) < groupMembers.indexOf(groupMemberA));
+ }
+
+ // Multiple sort fields not allowed.
+ {
+ // paging
+ Paging paging = getPaging(0, Integer.MAX_VALUE);
+ Map otherParams = new HashMap<>();
+ otherParams.put("orderBy", org.alfresco.rest.api.Groups.PARAM_ID + " ASC," + org.alfresco.rest.api.Groups.PARAM_DISPLAY_NAME + " ASC");
+
+ getGroupMembers(rootGroupName, paging, otherParams, "", HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ private void testGetGroupMembersSkipPaging() throws Exception
+ {
+ // +ve: check skip count.
+ {
+ // Sort params
+ Map otherParams = new HashMap<>();
+ addOrderBy(otherParams, org.alfresco.rest.api.Groups.PARAM_DISPLAY_NAME, false);
+
+ // Paging and list groups
+
+ int skipCount = 0;
+ int maxItems = 2;
+ Paging paging = getPaging(skipCount, maxItems);
+
+ ListResponse resp = getGroupMembers(rootGroupName, paging, otherParams);
+
+ // Paging and list groups with skip count.
+
+ skipCount = 1;
+ maxItems = 1;
+ paging = getPaging(skipCount, maxItems);
+
+ ListResponse sublistResponse = getGroupMembers(rootGroupName, paging, otherParams);
+
+ List expectedSublist = sublist(resp.getList(), skipCount, maxItems);
+ checkList(expectedSublist, sublistResponse.getPaging(), sublistResponse);
+ }
+
+ // -ve: check skip count.
+ {
+ getGroupMembers(rootGroupName, getPaging(-1, null), null, "", HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ private void testGetGroupsByMemberType() throws Exception
+ {
+ testGetGroupsByMemberType(rootGroupName, org.alfresco.rest.api.Groups.PARAM_MEMBER_TYPE_GROUP);
+ testGetGroupsByMemberType(groupB.getId(), org.alfresco.rest.api.Groups.PARAM_MEMBER_TYPE_PERSON);
+
+ // Invalid member type
+ {
+ Map otherParams = new HashMap<>();
+ otherParams.put("where", "(memberType=invalidMemberType)");
+
+ getGroupMembers(rootGroupName, getPaging(0, 4), otherParams, "", HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+
+ private void testGetGroupsByMemberType(String groupId, String memberType) throws Exception
+ {
+ // Sort params
+ Map otherParams = new HashMap<>();
+ otherParams.put("where", "(memberType=" + memberType + ")");
+
+ // Paging
+ Paging paging = getPaging(0, 4);
+
+ ListResponse resp = getGroupMembers(groupId, paging, otherParams);
+ resp.getList().forEach(groupMember -> {
+ validateGroupMemberDefaultFields(groupMember);
+ assertEquals("memberType was expected to be " + memberType, memberType, groupMember.getMemberType());
+ });
+ }
+
+ private void validateGroupMemberDefaultFields(GroupMember groupMember)
+ {
+ assertNotNull(groupMember);
+ assertNotNull(groupMember.getId());
+ assertNotNull(groupMember.getDisplayName());
+ assertNotNull(groupMember.getMemberType());
+ }
+
}
diff --git a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java
index 2a12c2efe9..ee4a1f3a21 100644
--- a/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java
+++ b/source/test-java/org/alfresco/rest/api/tests/client/PublicApiClient.java
@@ -57,6 +57,7 @@ import org.alfresco.rest.api.tests.client.data.Favourite;
import org.alfresco.rest.api.tests.client.data.FavouriteSite;
import org.alfresco.rest.api.tests.client.data.FolderNode;
import org.alfresco.rest.api.tests.client.data.Group;
+import org.alfresco.rest.api.tests.client.data.GroupMember;
import org.alfresco.rest.api.tests.client.data.JSONAble;
import org.alfresco.rest.api.tests.client.data.MemberOfSite;
import org.alfresco.rest.api.tests.client.data.NodeRating;
@@ -2262,9 +2263,25 @@ public class PublicApiClient
return null;
}
- public ListResponse getGroups(Map params) throws PublicApiException, ParseException
+ public ListResponse getGroupMembers(String groupId, Map params) throws PublicApiException, ParseException
{
- return getGroups(params, "Failed to get groups", HttpServletResponse.SC_OK);
+ return getGroupMembers(groupId, params, "Failed to get groups", HttpServletResponse.SC_OK);
+ }
+
+ public ListResponse getGroupMembers(String groupId, Map params, String errorMessage, int expectedStatus)
+ throws PublicApiException, ParseException
+ {
+ HttpResponse response = getAll("groups", groupId, "members", null, params, errorMessage, expectedStatus);
+
+ if (response != null && response.getJsonResponse() != null)
+ {
+ JSONObject jsonList = (JSONObject) response.getJsonResponse().get("list");
+ if (jsonList != null)
+ {
+ return GroupMember.parseGroupMembers(response.getJsonResponse());
+ }
+ }
+ return null;
}
}
}
diff --git a/source/test-java/org/alfresco/rest/api/tests/client/data/GroupMember.java b/source/test-java/org/alfresco/rest/api/tests/client/data/GroupMember.java
new file mode 100644
index 0000000000..c145a5712e
--- /dev/null
+++ b/source/test-java/org/alfresco/rest/api/tests/client/data/GroupMember.java
@@ -0,0 +1,107 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.rest.api.tests.client.data;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.rest.api.tests.client.PublicApiClient.ExpectedPaging;
+import org.alfresco.rest.api.tests.client.PublicApiClient.ListResponse;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+/**
+ * Represents a group member.
+ *
+ * @author cturlica
+ *
+ */
+public class GroupMember extends org.alfresco.rest.api.model.GroupMember implements Serializable, ExpectedComparison
+{
+
+ @Override
+ public void expected(Object o)
+ {
+ assertTrue("o is an instance of " + o.getClass(), o instanceof GroupMember);
+
+ GroupMember other = (GroupMember) o;
+
+ AssertUtil.assertEquals("id", getId(), other.getId());
+ AssertUtil.assertEquals("displayName", getDisplayName(), other.getDisplayName());
+ AssertUtil.assertEquals("memberType", getMemberType(), other.getMemberType());
+ }
+
+ public JSONObject toJSON()
+ {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("id", getId());
+ jsonObject.put("displayName", getDisplayName());
+ jsonObject.put("memberType", getMemberType());
+
+ return jsonObject;
+ }
+
+ public static GroupMember parseGroupMember(JSONObject jsonObject)
+ {
+ String id = (String) jsonObject.get("id");
+ String displayName = (String) jsonObject.get("displayName");
+ String memberType = (String) jsonObject.get("memberType");
+
+ GroupMember group = new GroupMember();
+ group.setId(id);
+ group.setDisplayName(displayName);
+ group.setMemberType(memberType);
+
+ return group;
+ }
+
+ public static ListResponse parseGroupMembers(JSONObject jsonObject)
+ {
+ List groupMembers = new ArrayList<>();
+
+ JSONObject jsonList = (JSONObject) jsonObject.get("list");
+ assertNotNull(jsonList);
+
+ JSONArray jsonEntries = (JSONArray) jsonList.get("entries");
+ assertNotNull(jsonEntries);
+
+ for (int i = 0; i < jsonEntries.size(); i++)
+ {
+ JSONObject jsonEntry = (JSONObject) jsonEntries.get(i);
+ JSONObject entry = (JSONObject) jsonEntry.get("entry");
+ groupMembers.add(parseGroupMember(entry));
+ }
+
+ ExpectedPaging paging = ExpectedPaging.parsePagination(jsonList);
+ ListResponse resp = new ListResponse<>(paging, groupMembers);
+ return resp;
+ }
+
+}