GroupsEndpoint.java
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you 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://opensource.org/licenses/ecl2.txt
*
* 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.opencastproject.external.endpoint;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
import static org.opencastproject.external.common.ApiVersion.VERSION_1_6_0;
import static org.opencastproject.index.service.util.JSONUtils.safeString;
import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
import org.opencastproject.external.common.ApiMediaType;
import org.opencastproject.external.common.ApiResponseBuilder;
import org.opencastproject.external.common.ApiVersion;
import org.opencastproject.index.service.resources.list.query.GroupsListQuery;
import org.opencastproject.index.service.util.RestUtils;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.UnauthorizedException;
import org.opencastproject.security.impl.jpa.JpaGroup;
import org.opencastproject.userdirectory.ConflictException;
import org.opencastproject.userdirectory.JpaGroupRoleProvider;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.doc.rest.RestParameter;
import org.opencastproject.util.doc.rest.RestQuery;
import org.opencastproject.util.doc.rest.RestResponse;
import org.opencastproject.util.doc.rest.RestService;
import org.opencastproject.util.requests.SortCriterion;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.collections4.ComparatorUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
@Path("/api/groups")
@Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
@RestService(name = "externalapigroups", title = "External API Groups Service", notes = {}, abstractText = "Provides resources and operations related to the groups")
@Component(
immediate = true,
service = GroupsEndpoint.class,
property = {
"service.description=External API - Groups Endpoint",
"opencast.service.type=org.opencastproject.external.groups",
"opencast.service.path=/api/groups"
}
)
@JaxrsResource
public class GroupsEndpoint {
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
/* OSGi service references */
private ElasticsearchIndex elasticsearchIndex;
private JpaGroupRoleProvider jpaGroupRoleProvider;
private SecurityService securityService;
/** OSGi DI */
@Reference
void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
this.elasticsearchIndex = elasticsearchIndex;
}
/** OSGi DI. */
@Reference
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
/** OSGi DI */
@Reference
public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
this.jpaGroupRoleProvider = jpaGroupRoleProvider;
}
/** OSGi activation method */
@Activate
void activate() {
logger.info("Activating External API - Groups Endpoint");
}
@GET
@Path("")
@RestQuery(name = "getgroups", description = "Returns a list of groups.", returnDescription = "", restParameters = {
@RestParameter(name = "filter", isRequired = false, description = "A comma seperated list of filters to limit the results with. A filter is the filter's name followed by a colon \":\" and then the value to filter with so it is the form [Filter Name]:[Value to Filter With].", type = STRING),
@RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting criteria. In the comma seperated list each type of sorting is specified as a pair such as: <Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or descending order and is mandatory.", isRequired = false, type = STRING),
@RestParameter(name = "limit", description = "The maximum number of results to return for a single request.", isRequired = false, type = RestParameter.Type.INTEGER),
@RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false, type = RestParameter.Type.INTEGER) }, responses = {
@RestResponse(description = "A (potentially empty) list of groups.", responseCode = HttpServletResponse.SC_OK) })
public Response getGroups(@HeaderParam("Accept") String acceptHeader, @QueryParam("filter") String filter,
@QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) {
final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
Optional<Integer> optLimit = Optional.ofNullable(limit);
Optional<Integer> optOffset = Optional.ofNullable(offset);
if (optLimit.isPresent() && limit <= 0) {
optLimit = Optional.empty();
}
if (optOffset.isPresent() && offset < 0) {
optOffset = Optional.empty();
}
// The API currently does not offer full text search for groups
Map<String, String> filters = new HashMap<>();
if (StringUtils.isNotBlank(filter)) {
for (String f : filter.split(",")) {
String[] filterTuple = f.split(":");
if (filterTuple.length < 2) {
logger.debug("No value for filter '{}' in filters list: {}", filterTuple[0], filter);
continue;
}
// use substring because dates also contain : so there might be more than two parts
filters.put(filterTuple[0].trim(), f.substring(filterTuple[0].length() + 1).trim());
}
}
Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
// sorting by members & roles is not supported by the database, so we do this afterwards
Set<SortCriterion> deprecatedSortCriteria = new HashSet<>();
if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
deprecatedSortCriteria = sortCriteria.stream().filter(
sortCriterion -> sortCriterion.getFieldName().equals("members")
|| sortCriterion.getFieldName().equals("roles")).collect(Collectors.toSet());
sortCriteria.removeAll(deprecatedSortCriteria);
}
List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter,
Optional.empty(), Optional.empty(), sortCriteria);
// sorting by members & roles is only available for api versions < 1.6.0
if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
List<Comparator<JpaGroup>> comparators = new ArrayList<>();
for (SortCriterion sortCriterion : deprecatedSortCriteria) {
Comparator<JpaGroup> comparator;
switch (sortCriterion.getFieldName()) {
case "members":
comparator = new GroupComparator() {
@Override
public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
return jpaGroup.getMembers();
}
};
break;
case "roles":
comparator = new GroupComparator() {
@Override
public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
return jpaGroup.getRoleNames();
}
};
break;
default:
continue;
}
if (sortCriterion.getOrder() == SortCriterion.Order.Descending) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
Collections.sort(results, ComparatorUtils.chainedComparator(comparators));
}
List<JsonObject> groupsJson = new ArrayList<>();
for (JpaGroup group : results) {
JsonObject groupJson = new JsonObject();
groupJson.addProperty("identifier", group.getGroupId());
groupJson.addProperty("organization", group.getOrganization().getId());
groupJson.addProperty("role", group.getRole());
groupJson.addProperty("name", safeString(group.getName()));
groupJson.addProperty("description", safeString(group.getDescription()));
groupJson.addProperty("roles", group.getRoleNames() != null ? String.join(",", group.getRoleNames()) : "");
groupJson.addProperty("members", group.getMembers() != null ? String.join(",", group.getMembers()) : "");
groupsJson.add(groupJson);
}
JsonArray responseArray = new JsonArray();
groupsJson.forEach(responseArray::add);
return ApiResponseBuilder.Json.ok(acceptHeader, responseArray);
}
/**
* Compare groups by set attributes like members or roles.
*/
private abstract class GroupComparator implements Comparator<JpaGroup> {
public abstract Set<String> getGroupAttribute(JpaGroup jpaGroup);
@Override
public int compare(JpaGroup group1, JpaGroup group2) {
List<String> members1 = new ArrayList<>(getGroupAttribute(group1));
List<String> members2 = new ArrayList<>(getGroupAttribute(group2));
for (int i = 0; i < members1.size() && i < members2.size(); i++) {
String member1 = members1.get(i);
String member2 = members2.get(i);
int result = member1.compareTo(member2);
if (result != 0) {
return result;
}
}
return (members1.size() - members2.size());
}
}
@GET
@Path("{groupId}")
@RestQuery(name = "getgroup", description = "Returns a single group.", returnDescription = "", pathParameters = {
@RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The group is returned.", responseCode = HttpServletResponse.SC_OK),
@RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response getGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id) {
JpaGroup group = jpaGroupRoleProvider.getGroup(id);
if (group == null) {
return ApiResponseBuilder.notFound("Cannot find a group with id '%s'.", id);
}
JsonObject json = new JsonObject();
json.addProperty("identifier", group.getGroupId());
json.addProperty("organization", group.getOrganization().getId());
json.addProperty("role", group.getRole());
json.addProperty("name", safeString(group.getName()));
json.addProperty("description", safeString(group.getDescription()));
json.addProperty("roles", safeString(group.getRoleNames()));
json.addProperty("members", safeString(group.getMembers()));
return ApiResponseBuilder.Json.ok(acceptHeader, json);
}
@DELETE
@Path("{groupId}")
@RestQuery(name = "deletegroup", description = "Deletes a group.", returnDescription = "", pathParameters = {
@RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The group has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response deleteGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id)
throws NotFoundException {
try {
jpaGroupRoleProvider.removeGroup(id);
return Response.noContent().build();
} catch (NotFoundException e) {
return Response.status(SC_NOT_FOUND).build();
} catch (UnauthorizedException e) {
return Response.status(SC_FORBIDDEN).build();
} catch (Exception e) {
logger.error("Unable to delete group {}", id, e);
throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
}
}
@PUT
@Path("{groupId}")
@RestQuery(name = "updategroup", description = "Updates a group.", returnDescription = "", pathParameters = {
@RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "name", isRequired = false, description = "Group Name", type = STRING),
@RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
@RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false, type = STRING),
@RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false, type = STRING) }, responses = {
@RestResponse(description = "The group has been updated.", responseCode = HttpServletResponse.SC_CREATED),
@RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_BAD_REQUEST) })
public Response updateGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
@FormParam("name") String name, @FormParam("description") String description,
@FormParam("roles") String roles, @FormParam("members") String members) throws Exception {
try {
jpaGroupRoleProvider.updateGroup(id, name, description, roles, members);
} catch (IllegalArgumentException e) {
logger.warn("Unable to update group id {}: {}", id, e.getMessage());
return Response.status(SC_BAD_REQUEST).build();
} catch (UnauthorizedException ex) {
return Response.status(SC_FORBIDDEN).build();
}
return Response.ok().build();
}
@POST
@Path("")
@RestQuery(name = "creategroup", description = "Creates a group.", returnDescription = "", restParameters = {
@RestParameter(name = "name", isRequired = true, description = "Group Name", type = STRING),
@RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
@RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false, type = STRING),
@RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false, type = STRING) }, responses = {
@RestResponse(description = "A new group is created.", responseCode = SC_CREATED),
@RestResponse(description = "The request is invalid or inconsistent.", responseCode = SC_BAD_REQUEST) })
public Response createGroup(@HeaderParam("Accept") String acceptHeader, @FormParam("name") String name,
@FormParam("description") String description, @FormParam("roles") String roles,
@FormParam("members") String members) {
try {
jpaGroupRoleProvider.createGroup(name, description, roles, members);
} catch (IllegalArgumentException e) {
logger.warn("Unable to create group {}: {}", name, e.getMessage());
return Response.status(SC_BAD_REQUEST).build();
} catch (UnauthorizedException e) {
return Response.status(SC_FORBIDDEN).build();
} catch (ConflictException e) {
return Response.status(SC_CONFLICT).build();
}
return Response.status(SC_CREATED).build();
}
@POST
@Path("{groupId}/members")
@RestQuery(name = "addgroupmember", description = "Adds a member to a group.", returnDescription = "", pathParameters = {
@RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, restParameters = {
@RestParameter(name = "member", description = "Member Name", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The member was already member of the group.", responseCode = SC_OK),
@RestResponse(description = "The member has been added.", responseCode = SC_NO_CONTENT),
@RestResponse(description = "The specified group does not exist.", responseCode = SC_NOT_FOUND) })
public Response addGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
@FormParam("member") String member) {
try {
if (jpaGroupRoleProvider.addMemberToGroup(id, member)) {
return Response.ok().build();
} else {
return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already member of group.");
}
} catch (IllegalArgumentException e) {
logger.warn("Unable to add member to group id {}.", id, e);
return Response.status(SC_BAD_REQUEST).build();
} catch (UnauthorizedException ex) {
return Response.status(SC_FORBIDDEN).build();
} catch (NotFoundException e) {
return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
} catch (Exception e) {
logger.warn("Could not update the group with id {}.",id, e);
return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'",id,getMessage(e));
}
}
@DELETE
@Path("{groupId}/members/{memberId}")
@RestQuery(name = "removegroupmember", description = "Removes a member from a group", returnDescription = "", pathParameters = {
@RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING),
@RestParameter(name = "memberId", description = "The member id", isRequired = true, type = STRING) }, responses = {
@RestResponse(description = "The member has been removed.", responseCode = HttpServletResponse.SC_NO_CONTENT),
@RestResponse(description = "The specified group or member does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
public Response removeGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
@PathParam("memberId") String memberId) {
try {
if (jpaGroupRoleProvider.removeMemberFromGroup(id, memberId)) {
return Response.ok().build();
} else {
return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already not member of group.");
}
} catch (IllegalArgumentException e) {
logger.warn("Unable to remove member from group id {}.", id, e);
return Response.status(SC_BAD_REQUEST).build();
} catch (UnauthorizedException ex) {
return Response.status(SC_FORBIDDEN).build();
} catch (NotFoundException e) {
return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
} catch (Exception e) {
logger.warn("Could not update the group with id {}.", id, e);
return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'", id, getMessage(e));
}
}
}