blob: 39701fa1684009390d0924392008b5a5b2e50ff1 [file] [log] [blame]
package edu.psu.swe.scim.server.rest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.UriInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import edu.psu.swe.scim.server.exception.AttributeDoesNotExistException;
import edu.psu.swe.scim.server.exception.UnableToCreateResourceException;
import edu.psu.swe.scim.server.exception.UnableToDeleteResourceException;
import edu.psu.swe.scim.server.exception.UnableToRetrieveResourceException;
import edu.psu.swe.scim.server.exception.UnableToUpdateResourceException;
import edu.psu.swe.scim.server.provider.Provider;
import edu.psu.swe.scim.server.provider.annotations.ScimProcessingExtension;
import edu.psu.swe.scim.server.provider.extensions.AttributeFilterExtension;
import edu.psu.swe.scim.server.provider.extensions.ProcessingExtension;
import edu.psu.swe.scim.server.provider.extensions.ScimRequestContext;
import edu.psu.swe.scim.server.provider.extensions.exceptions.ClientFilterException;
import edu.psu.swe.scim.server.utility.AttributeUtil;
import edu.psu.swe.scim.server.utility.EndpointUtil;
import edu.psu.swe.scim.server.utility.EtagGenerator;
import edu.psu.swe.scim.spec.protocol.BaseResourceTypeResource;
import edu.psu.swe.scim.spec.protocol.attribute.AttributeReference;
import edu.psu.swe.scim.spec.protocol.attribute.AttributeReferenceListWrapper;
import edu.psu.swe.scim.spec.protocol.data.ListResponse;
import edu.psu.swe.scim.spec.protocol.data.PatchRequest;
import edu.psu.swe.scim.spec.protocol.data.SearchRequest;
import edu.psu.swe.scim.spec.protocol.filter.FilterResponse;
import edu.psu.swe.scim.spec.protocol.search.Filter;
import edu.psu.swe.scim.spec.protocol.search.PageRequest;
import edu.psu.swe.scim.spec.protocol.search.SortOrder;
import edu.psu.swe.scim.spec.protocol.search.SortRequest;
import edu.psu.swe.scim.spec.resources.ScimResource;
import edu.psu.swe.scim.spec.schema.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> implements BaseResourceTypeResource<T> {
private static final Logger LOG = LoggerFactory.getLogger(BaseResourceTypeResourceImpl.class);
public abstract Provider<T> getProvider();
@Context
UriInfo uriInfo;
@Context
Request request;
@Context
HttpServletRequest servletRequest;
@Inject
AttributeUtil attributeUtil;
@Inject
EndpointUtil endpointUtil;
@Inject
EtagGenerator etagGenerator;
@Override
public Response getById(String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) {
Provider<T> provider = null;
if (servletRequest.getParameter("filter") != null) {
return Response.status(Status.FORBIDDEN).build();
}
provider = getProvider();
if (provider == null) {
return BaseResourceTypeResource.super.getById(id, attributes, excludedAttributes);
}
endpointUtil.process(uriInfo);
T resource = null;
try {
resource = provider.get(id);
} catch (UnableToRetrieveResourceException e2) {
if (e2.getStatus().getFamily().equals(Family.SERVER_ERROR)) {
return createGenericExceptionResponse(e2, e2.getStatus());
}
}
if (resource != null) {
EntityTag backingETag = null;
try {
backingETag = etagGenerator.generateEtag(resource);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e1) {
return createETagErrorResponse();
}
ResponseBuilder evaluatePreconditionsResponse = request.evaluatePreconditions(backingETag);
if (evaluatePreconditionsResponse != null) {
return Response.status(Status.NOT_MODIFIED).build();
}
}
Set<AttributeReference> attributeReferences = Optional.ofNullable(attributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
Set<AttributeReference> excludedAttributeReferences = Optional.ofNullable(excludedAttributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
return createAmbiguousAttributeParametersResponse();
}
if (resource == null) {
return createNotFoundResponse(id);
}
EntityTag etag = null;
try {
etag = etagGenerator.generateEtag(resource);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
return createETagErrorResponse();
}
// Process Attributes
try {
resource = processFilterAttributeExtensions(provider, resource, attributeReferences, excludedAttributeReferences);
} catch (ClientFilterException e1) {
ErrorResponse er = new ErrorResponse();
er.setStatus(Integer.toString(e1.getStatus().getStatusCode()));
er.setDetail(e1.getMessage());
return Response.status(e1.getStatus()).entity(er).build();
}
try {
if (!excludedAttributeReferences.isEmpty()) {
resource = attributeUtil.setExcludedAttributesForDisplay(resource, excludedAttributeReferences);
} else {
resource = attributeUtil.setAttributesForDisplay(resource, attributeReferences);
}
return Response.ok().entity(resource).location(buildLocationTag(resource)).tag(etag).build();
} catch (IllegalArgumentException | IllegalAccessException | AttributeDoesNotExistException | IOException e) {
e.printStackTrace();
return createAttriubteProcessingErrorResponse(e);
}
}
@Override
public Response query(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes, Filter filter, AttributeReference sortBy, SortOrder sortOrder, Integer startIndex, Integer count) {
SearchRequest searchRequest = new SearchRequest();
searchRequest.setAttributes(Optional.ofNullable(attributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet()));
searchRequest.setExcludedAttributes(Optional.ofNullable(excludedAttributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet()));
searchRequest.setFilter(filter);
searchRequest.setSortBy(sortBy);
searchRequest.setSortOrder(sortOrder);
searchRequest.setStartIndex(startIndex);
searchRequest.setCount(count);
return find(searchRequest);
}
@Override
public Response create(T resource, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) {
Provider<T> provider = null;
provider = getProvider();
if (provider == null) {
return BaseResourceTypeResource.super.create(resource, attributes, excludedAttributes);
}
Set<AttributeReference> attributeReferences = Optional.ofNullable(attributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
Set<AttributeReference> excludedAttributeReferences = Optional.ofNullable(excludedAttributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
return createAmbiguousAttributeParametersResponse();
}
endpointUtil.process(uriInfo);
T created;
try {
created = provider.create(resource);
} catch (UnableToCreateResourceException e1) {
ErrorResponse er = new ErrorResponse();
Status status = e1.getStatus();
if (e1.getStatus().equals(Status.CONFLICT)) {
er.setStatus(e1.getStatus().toString());
er.setDetail("uniqueness");
} else {
er.setStatus(e1.getStatus().toString());
er.setDetail(e1.getMessage());
}
return Response.status(status).entity(er).build();
}
EntityTag etag = null;
try {
etag = etagGenerator.generateEtag(created);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.error("Failed to generate etag for newly created entity " + e.getMessage());
}
// Process Attributes
try {
resource = processFilterAttributeExtensions(provider, resource, attributeReferences, excludedAttributeReferences);
} catch (ClientFilterException e1) {
ErrorResponse er = new ErrorResponse();
er.setStatus(Integer.toString(e1.getStatus().getStatusCode()));
er.setDetail(e1.getMessage());
return Response.status(e1.getStatus()).entity(er).build();
}
try {
if (!excludedAttributeReferences.isEmpty()) {
created = attributeUtil.setExcludedAttributesForDisplay(created, excludedAttributeReferences);
} else {
created = attributeUtil.setAttributesForDisplay(created, attributeReferences);
}
} catch (IllegalArgumentException | IllegalAccessException | AttributeDoesNotExistException | IOException e) {
if (etag == null) {
return Response.status(Status.CREATED).location(buildLocationTag(created)).build();
} else {
Response.status(Status.CREATED).location(buildLocationTag(created)).tag(etag).build();
}
}
// TODO - Is this the right behavior?
if (etag == null) {
return Response.status(Status.CREATED).location(buildLocationTag(created)).entity(created).build();
}
return Response.status(Status.CREATED).location(buildLocationTag(created)).tag(etag).entity(created).build();
}
@Override
public Response find(SearchRequest request) {
Provider<T> provider = null;
provider = getProvider();
if (provider == null) {
return BaseResourceTypeResource.super.find(request);
}
Set<AttributeReference> attributeReferences = Optional.ofNullable(request.getAttributes()).orElse(Collections.emptySet());
Set<AttributeReference> excludedAttributeReferences = Optional.ofNullable(request.getExcludedAttributes()).orElse(Collections.emptySet());
if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
return createAmbiguousAttributeParametersResponse();
}
Filter filter = request.getFilter();
PageRequest pageRequest = request.getPageRequest();
SortRequest sortRequest = request.getSortRequest();
ListResponse listResponse = new ListResponse();
endpointUtil.process(uriInfo);
FilterResponse<T> filterResp = null;
try {
filterResp = provider.find(filter, pageRequest, sortRequest);
} catch (UnableToRetrieveResourceException e1) {
log.info("Caught an UnableToRetrieveResourceException " + e1.getMessage() + " : " + e1.getStatus().toString());
return createGenericExceptionResponse(e1, e1.getStatus());
}
// If no resources are found, we should still return a ListResponse with
// the totalResults set to 0;
// (https://tools.ietf.org/html/rfc7644#section-3.4.2)
if (filterResp == null || filterResp.getResources() == null || filterResp.getResources().isEmpty()) {
listResponse.setTotalResults(0);
} else {
log.info("Find returned " + filterResp.getResources().size());
listResponse.setItemsPerPage(filterResp.getResources().size());
listResponse.setStartIndex(1);
listResponse.setTotalResults(filterResp.getResources().size());
List<Object> results = new ArrayList<>();
for (T resource : filterResp.getResources()) {
EntityTag etag = null;
try {
etag = etagGenerator.generateEtag(resource);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
return createETagErrorResponse();
}
// Process Attributes
try {
log.info("=== Calling processFilterAttributeExtensions");
resource = processFilterAttributeExtensions(provider, resource, attributeReferences, excludedAttributeReferences);
} catch (ClientFilterException e1) {
ErrorResponse er = new ErrorResponse();
er.setStatus(Integer.toString(e1.getStatus().getStatusCode()));
er.setDetail(e1.getMessage());
return Response.status(e1.getStatus()).entity(er).build();
}
try {
if (!excludedAttributeReferences.isEmpty()) {
resource = attributeUtil.setExcludedAttributesForDisplay(resource, excludedAttributeReferences);
} else {
resource = attributeUtil.setAttributesForDisplay(resource, attributeReferences);
}
results.add(resource);
} catch (IllegalArgumentException | IllegalAccessException | AttributeDoesNotExistException | IOException e) {
return createAttriubteProcessingErrorResponse(e);
}
}
listResponse.setResources(results);
}
return Response.ok().entity(listResponse).build();
}
@Override
public Response update(T resource, String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) {
Provider<T> provider = null;
provider = getProvider();
if (provider == null) {
return BaseResourceTypeResource.super.update(resource, id, attributes, excludedAttributes);
}
Set<AttributeReference> attributeReferences = Optional.ofNullable(attributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
Set<AttributeReference> excludedAttributeReferences = Optional.ofNullable(excludedAttributes).map(wrapper -> wrapper.getAttributeReferences()).orElse(Collections.emptySet());
if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
return createAmbiguousAttributeParametersResponse();
}
endpointUtil.process(uriInfo);
T stored;
try {
stored = provider.get(id);
} catch (UnableToRetrieveResourceException e2) {
return createGenericExceptionResponse(e2, e2.getStatus());
}
if (stored == null) {
return createNotFoundResponse(id);
}
EntityTag backingETag = null;
try {
backingETag = etagGenerator.generateEtag(stored);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e1) {
return createETagErrorResponse();
}
ResponseBuilder evaluatePreconditionsResponse = request.evaluatePreconditions(backingETag);
if (evaluatePreconditionsResponse != null) {
return createPreconditionFailedResponse(resource, evaluatePreconditionsResponse);
}
T updated;
try {
updated = provider.update(id, resource);
} catch (UnableToUpdateResourceException e1) {
return createGenericExceptionResponse(e1, e1.getStatus());
}
// Process Attributes
try {
resource = processFilterAttributeExtensions(provider, resource, attributeReferences, excludedAttributeReferences);
} catch (ClientFilterException e1) {
ErrorResponse er = new ErrorResponse();
er.setStatus(Integer.toString(e1.getStatus().getStatusCode()));
er.setDetail(e1.getMessage());
return Response.status(e1.getStatus()).entity(er).build();
}
try {
if (!excludedAttributeReferences.isEmpty()) {
updated = attributeUtil.setExcludedAttributesForDisplay(updated, excludedAttributeReferences);
} else {
updated = attributeUtil.setAttributesForDisplay(updated, attributeReferences);
}
} catch (IllegalArgumentException | IllegalAccessException | AttributeDoesNotExistException | IOException e) {
log.error("Failed to handle attribute processing in update " + e.getMessage());
}
EntityTag etag = null;
try {
etag = etagGenerator.generateEtag(updated);
} catch (JsonProcessingException | NoSuchAlgorithmException | UnsupportedEncodingException e) {
log.error("Failed to generate etag for newly created entity " + e.getMessage());
}
// TODO - Is this correct or should we support roll back semantics
if (etag == null) {
return Response.ok(updated).location(buildLocationTag(updated)).build();
}
return Response.ok(updated).location(buildLocationTag(updated)).tag(etag).build();
}
@Override
public Response patch(PatchRequest patchRequest) {
// TODO Auto-generated method stub
return BaseResourceTypeResource.super.patch(patchRequest);
}
@Override
public Response delete(String id) {
try {
Response response;
Provider<T> provider = getProvider();
if (provider == null) {
response = BaseResourceTypeResource.super.delete(id);
} else {
endpointUtil.process(uriInfo);
response = Response.noContent().build();
provider.delete(id);
}
return response;
} catch (UnableToDeleteResourceException e) {
Status status = e.getStatus();
Response response = Response.status(status).build();
log.error("Unable to delete resource", e);
return response;
}
}
private T processFilterAttributeExtensions(Provider<T> provider, T resource, Set<AttributeReference> attributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ClientFilterException {
ScimProcessingExtension annotation = provider.getClass().getAnnotation(ScimProcessingExtension.class);
if (annotation != null) {
Class<? extends ProcessingExtension>[] value = annotation.value();
for (Class<? extends ProcessingExtension> class1 : value) {
try {
ProcessingExtension processingExtension = class1.newInstance();
if (processingExtension instanceof AttributeFilterExtension) {
AttributeFilterExtension attributeFilterExtension = (AttributeFilterExtension) processingExtension;
ScimRequestContext scimRequestContext = new ScimRequestContext(attributeReferences, excludedAttributeReferences);
resource = (T) attributeFilterExtension.filterAttributes(resource, scimRequestContext);
log.info("Resource now - " + resource.toString());
}
} catch (InstantiationException | IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return resource;
}
private URI buildLocationTag(T resource) {
String id = resource.getId();
if (id == null) {
LOG.warn("Provider must supply an id for a resource");
id = "unknown";
}
return uriInfo.getAbsolutePathBuilder().path(id).build();
}
private Response createGenericExceptionResponse(Exception e1, Status status) {
ErrorResponse er = new ErrorResponse();
er.setDetail(e1.getLocalizedMessage());
if (status != null) {
er.setStatus(Integer.toString(status.getStatusCode()));
return Response.status(status).entity(er).build();
} else {
er.setStatus("500");
return Response.status(Status.BAD_REQUEST).entity(er).build();
}
}
private Response createAmbiguousAttributeParametersResponse() {
ErrorResponse er = new ErrorResponse();
er.setStatus("400");
er.setDetail("Cannot include both attributes and excluded attributes in a single request");
return Response.status(Status.BAD_REQUEST).entity(er).build();
}
private Response createNotFoundResponse(String id) {
ErrorResponse er = new ErrorResponse();
er.setStatus("404");
er.setDetail("Resource " + id + " not found");
return Response.status(Status.NOT_FOUND).entity(er).build();
}
private Response createETagErrorResponse() {
ErrorResponse er = new ErrorResponse();
er.setStatus("500");
er.setDetail("Failed to generate the etag");
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(er).build();
}
private Response createAttriubteProcessingErrorResponse(Exception e) {
ErrorResponse er = new ErrorResponse();
er.setStatus("500");
er.setDetail("Failed to parse the attribute query value " + e.getMessage());
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(er).build();
}
private Response createPreconditionFailedResponse(T resource, ResponseBuilder evaluatePreconditionsResponse) {
ErrorResponse er = new ErrorResponse();
er.setStatus("412");
er.setDetail("Failed to update record, backing record has changed - " + resource.getId());
log.warn("Failed to update record, backing record has changed - " + resource.getId());
return evaluatePreconditionsResponse.entity(er).build();
}
}