com.unboundid.scim.sdk.SCIMEndpoint Maven / Gradle / Ivy
/*
* Copyright 2011-2019 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.scim.sdk;
import com.unboundid.scim.data.Meta;
import com.unboundid.scim.data.ResourceFactory;
import com.unboundid.scim.data.BaseResource;
import com.unboundid.scim.facade.org.apache.wink.client.Resource;
import com.unboundid.scim.marshal.Marshaller;
import com.unboundid.scim.marshal.Unmarshaller;
import com.unboundid.scim.marshal.json.JsonMarshaller;
import com.unboundid.scim.marshal.json.JsonUnmarshaller;
import com.unboundid.scim.marshal.xml.XmlMarshaller;
import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
import com.unboundid.scim.schema.ResourceDescriptor;
import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.NoHttpResponseException;
import org.apache.http.UnsupportedHttpVersionException;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.RedirectException;
import com.unboundid.scim.facade.org.apache.wink.client.
ClientAuthenticationException;
import com.unboundid.scim.facade.org.apache.wink.client.ClientConfigException;
import com.unboundid.scim.facade.org.apache.wink.client.ClientResponse;
import com.unboundid.scim.facade.org.apache.wink.client.ClientRuntimeException;
import com.unboundid.scim.facade.org.apache.wink.client.ClientWebException;
import com.unboundid.scim.facade.org.apache.wink.client.RestClient;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import static com.unboundid.scim.sdk.SCIMConstants.*;
/**
* This class represents a SCIM endpoint (ie. Users, Groups, etc.) and handles
* all protocol-level interactions with the service provider. It acts as a
* helper class for invoking CRUD operations of resources and processing their
* results.
*
* @param The type of resource instances handled by this SCIMEndpoint.
*/
public class SCIMEndpoint
{
private final SCIMService scimService;
private final ResourceDescriptor resourceDescriptor;
private final ResourceFactory resourceFactory;
private final Unmarshaller unmarshaller;
private final Marshaller marshaller;
private final MediaType contentType;
private final MediaType acceptType;
private final boolean[] overrides = new boolean[3];
private final RestClient client;
private final boolean useUrlSuffix;
/**
* Create a SCIMEndpoint with the provided information.
*
* @param scimService The SCIMService to use.
* @param restClient The Wink REST client.
* @param resourceDescriptor The resource descriptor of this endpoint.
* @param resourceFactory The ResourceFactory that should be used to create
* resource instances.
*/
SCIMEndpoint(final SCIMService scimService,
final RestClient restClient,
final ResourceDescriptor resourceDescriptor,
final ResourceFactory resourceFactory)
{
this.scimService = scimService;
this.client = restClient;
this.resourceDescriptor = resourceDescriptor;
this.resourceFactory = resourceFactory;
this.contentType = scimService.getContentType();
this.acceptType = scimService.getAcceptType();
this.overrides[0] = scimService.isOverridePut();
this.overrides[1] = scimService.isOverridePatch();
this.overrides[2] = scimService.isOverrideDelete();
this.useUrlSuffix = scimService.isUseUrlSuffix();
if (scimService.getContentType().equals(MediaType.APPLICATION_JSON_TYPE))
{
this.marshaller = new JsonMarshaller();
}
else
{
this.marshaller = new XmlMarshaller();
}
if(scimService.getAcceptType().equals(MediaType.APPLICATION_JSON_TYPE))
{
this.unmarshaller = new JsonUnmarshaller();
}
else
{
this.unmarshaller = new XmlUnmarshaller();
}
}
/**
* Constructs a new instance of a resource object which is empty. This
* method does not interact with the SCIM service. It creates a local object
* that may be provided to the {@link SCIMEndpoint#create} method after the
* attributes have been specified.
*
* @return A new instance of a resource object.
*/
public R newResource()
{
return resourceFactory.createResource(resourceDescriptor, new SCIMObject());
}
/**
* Retrieves a resource instance given the ID.
*
* @param id The ID of the resource to retrieve.
* @return The retrieved resource.
* @throws SCIMException If an error occurs.
*/
public R get(final String id)
throws SCIMException
{
return get(id, null, (String[]) null);
}
/**
* Retrieves a resource instance given the ID, only if the current version
* has been modified.
*
* @param id The ID of the resource to retrieve.
* @param etag The entity tag that indicates the entry should be returned
* only if the entity tag of the current resource is different
* from the provided value and a value of "*" will not return
* an entry if the resource still exists. A value of
* null
indicates unconditional return.
* @param requestedAttributes The attributes of the resource to retrieve.
* @return The retrieved resource.
* @throws SCIMException If an error occurs.
*/
public R get(final String id, final String etag,
final String... requestedAttributes)
throws SCIMException
{
final UriBuilder uriBuilder = UriBuilder.fromUri(scimService.getBaseURL());
uriBuilder.path(resourceDescriptor.getEndpoint());
// The ServiceProviderConfig is a special case where the id is not
// specified.
if (id != null)
{
uriBuilder.path(id);
}
URI uri = uriBuilder.build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
addAttributesQuery(clientResource, requestedAttributes);
if(scimService.getUserAgent() != null)
{
clientResource.header(HttpHeaders.USER_AGENT, scimService.getUserAgent());
}
if(etag != null && !etag.isEmpty())
{
clientResource.header(HttpHeaders.IF_NONE_MATCH, etag);
}
ClientResponse response = null;
try
{
response = clientResource.get();
InputStream entity = response.getEntity(InputStream.class);
if(response.getStatusType() == Response.Status.OK)
{
R resource = unmarshaller.unmarshal(entity, resourceDescriptor,
resourceFactory);
addMissingMetaData(response, resource);
return resource;
}
else
{
throw createErrorResponseException(response, entity);
}
}
catch(SCIMException e)
{
throw e;
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Retrieves all resource instances that match the provided filter.
*
* @param filter The filter that should be used.
* @return The resource instances that match the provided filter.
* @throws SCIMException If an error occurs.
*/
public Resources query(final String filter)
throws SCIMException
{
return query(filter, null, null);
}
/**
* Retrieves all resource instances that match the provided filter.
* Matching resources are returned sorted according to the provided
* SortParameters. PageParameters maybe used to specify the range of
* resource instances that are returned.
*
* @param filter The filter that should be used.
* @param sortParameters The sort parameters that should be used.
* @param pageParameters The page parameters that should be used.
* @param requestedAttributes The attributes of the resource to retrieve.
* @return The resource instances that match the provided filter.
* @throws SCIMException If an error occurs.
*/
public Resources query(final String filter,
final SortParameters sortParameters,
final PageParameters pageParameters,
final String... requestedAttributes)
throws SCIMException
{
return query(filter, sortParameters, pageParameters,
null, requestedAttributes);
}
/**
* Retrieves all resource instances that match the provided filter.
* Matching resources are returned sorted according to the provided
* SortParameters. PageParameters maybe used to specify the range of
* resource instances that are returned. Additional query parameters may
* be specified using a Map of parameter names to their values.
*
* @param filter The filter that should be used.
* @param sortParameters The sort parameters that should be used.
* @param pageParameters The page parameters that should be used.
* @param additionalQueryParams A map of additional query parameters that
* should be included.
* @param requestedAttributes The attributes of the resource to retrieve.
* @return The resource instances that match the provided filter.
* @throws SCIMException If an error occurs.
*/
public Resources query(final String filter,
final SortParameters sortParameters,
final PageParameters pageParameters,
final Map additionalQueryParams,
final String... requestedAttributes)
throws SCIMException
{
URI uri =
UriBuilder.fromUri(scimService.getBaseURL()).path(
resourceDescriptor.getEndpoint()).build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
addAttributesQuery(clientResource, requestedAttributes);
if(scimService.getUserAgent() != null)
{
clientResource.header("User-Agent", scimService.getUserAgent());
}
if(filter != null)
{
clientResource.queryParam(QUERY_PARAMETER_FILTER, filter);
}
if(sortParameters != null)
{
clientResource.queryParam(QUERY_PARAMETER_SORT_BY,
sortParameters.getSortBy().toString());
if(!sortParameters.isAscendingOrder())
{
clientResource.queryParam(QUERY_PARAMETER_SORT_ORDER,
sortParameters.getSortOrder());
}
}
if(pageParameters != null)
{
clientResource.queryParam(QUERY_PARAMETER_PAGE_START_INDEX,
String.valueOf(pageParameters.getStartIndex()));
if (pageParameters.getCount() > 0)
{
clientResource.queryParam(QUERY_PARAMETER_PAGE_SIZE,
String.valueOf(pageParameters.getCount()));
}
}
if(additionalQueryParams != null)
{
for (String key : additionalQueryParams.keySet())
{
clientResource.queryParam(key, additionalQueryParams.get(key));
}
}
ClientResponse response = null;
try
{
response = clientResource.get();
InputStream entity = response.getEntity(InputStream.class);
if(response.getStatusType() == Response.Status.OK)
{
return unmarshaller.unmarshalResources(entity, resourceDescriptor,
resourceFactory);
}
else
{
throw createErrorResponseException(response, entity);
}
}
catch(SCIMException e)
{
throw e;
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Create the specified resource instance at the service provider and return
* only the specified attributes from the newly inserted resource.
*
* @param resource The resource to create.
* @param requestedAttributes The attributes of the newly inserted resource
* to retrieve.
* @return The newly inserted resource returned by the service provider.
* @throws SCIMException If an error occurs.
*/
public R create(final R resource,
final String... requestedAttributes)
throws SCIMException
{
URI uri =
UriBuilder.fromUri(scimService.getBaseURL()).path(
resourceDescriptor.getEndpoint()).build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
addAttributesQuery(clientResource, requestedAttributes);
if(scimService.getUserAgent() != null)
{
clientResource.header("User-Agent", scimService.getUserAgent());
}
StreamingOutput output = new StreamingOutput() {
public void write(final OutputStream outputStream)
throws IOException, WebApplicationException {
try {
marshaller.marshal(resource, outputStream);
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
}
}
};
ClientResponse response = null;
try
{
response = clientResource.post(output);
InputStream entity = response.getEntity(InputStream.class);
if(response.getStatusType() == Response.Status.CREATED)
{
R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
resourceFactory);
addMissingMetaData(response, postedResource);
return postedResource;
}
else
{
throw createErrorResponseException(response, entity);
}
}
catch(SCIMException e)
{
throw e;
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Update the existing resource with the one provided (using the HTTP PUT
* method).
*
* @param resource The modified resource to be updated.
* @return The updated resource returned by the service provider.
* @throws SCIMException If an error occurs.
*/
public R update(final R resource)
throws SCIMException
{
if(resource.getId() == null)
{
throw new InvalidResourceException("Resource must have a valid ID");
}
return update(resource.getId(),
resource.getMeta() == null ?
null : resource.getMeta().getVersion(),
resource);
}
/**
* Update the existing resource with the one provided (using the HTTP PUT
* method). This update is conditional upon the provided entity tag matching
* the tag from the current resource. If (and only if) they match, the update
* will be performed.
*
* @param resource The modified resource to be updated.
* @param etag The entity tag value that is the expected value for the target
* resource. A value of null
will not set an
* etag precondition and a value of "*" will perform an
* unconditional update.
* @param requestedAttributes The attributes of updated resource
* to return.
* @return The updated resource returned by the service provider.
* @throws SCIMException If an error occurs.
*/
@Deprecated
public R update(final R resource, final String etag,
final String... requestedAttributes)
throws SCIMException
{
String id = resource.getId();
if(id == null)
{
throw new InvalidResourceException("Resource must have a valid ID");
}
return update(id, etag, resource, requestedAttributes);
}
/**
* Update the existing resource with the one provided (using the HTTP PUT
* method). This update is conditional upon the provided entity tag matching
* the tag from the current resource. If (and only if) they match, the update
* will be performed.
*
* @param id The ID of the resource to update.
* @param etag The entity tag value that is the expected value for the target
* resource. A value of null
will not set an
* etag precondition and a value of "*" will perform an
* unconditional update.
* @param resource The modified resource to be updated.
* @param requestedAttributes The attributes of updated resource
* to return.
* @return The updated resource returned by the service provider.
* @throws SCIMException If an error occurs.
*/
public R update(final String id, final String etag, final R resource,
final String... requestedAttributes)
throws SCIMException
{
URI uri =
UriBuilder.fromUri(scimService.getBaseURL()).path(
resourceDescriptor.getEndpoint()).path(id).build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
addAttributesQuery(clientResource, requestedAttributes);
if(scimService.getUserAgent() != null)
{
clientResource.header(HttpHeaders.USER_AGENT, scimService.getUserAgent());
}
if(etag != null && !etag.isEmpty())
{
clientResource.header(HttpHeaders.IF_MATCH, etag);
}
StreamingOutput output = new StreamingOutput() {
public void write(final OutputStream outputStream)
throws IOException, WebApplicationException {
try {
marshaller.marshal(resource, outputStream);
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
}
}
};
ClientResponse response = null;
try
{
if(overrides[0])
{
clientResource.header("X-HTTP-Method-Override", "PUT");
response = clientResource.post(output);
}
else
{
response = clientResource.put(output);
}
InputStream entity = response.getEntity(InputStream.class);
if(response.getStatusType() == Response.Status.OK)
{
R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
resourceFactory);
addMissingMetaData(response, postedResource);
return postedResource;
}
else
{
throw createErrorResponseException(response, entity);
}
}
catch(SCIMException e)
{
throw e;
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Update the existing resource with the one provided (using the HTTP PATCH
* method). Note that if the {@code attributesToDelete} parameter is
* specified, those attributes will be removed from the resource before the
* {@code attributesToUpdate} are merged into the resource.
*
* @param resource The resource to update.
* @param attributesToUpdate The list of attributes (and their new values) to
* update on the resource.
* @param attributesToDelete The list of attributes to delete on the resource.
* @return The updated resource returned by the service provider, or
* an empty resource with returned meta data if the service provider
* returned no content.
* @throws SCIMException If an error occurs.
*/
public R update(final R resource,
final List attributesToUpdate,
final List attributesToDelete)
throws SCIMException
{
if(resource.getId() == null)
{
throw new InvalidResourceException("Resource must have a valid ID");
}
return update(resource.getId(),
resource.getMeta() == null ?
null : resource.getMeta().getVersion(),
attributesToUpdate,
attributesToDelete);
}
/**
* Update the existing resource with the one provided (using the HTTP PATCH
* method). Note that if the {@code attributesToDelete} parameter is
* specified, those attributes will be removed from the resource before the
* {@code attributesToUpdate} are merged into the resource.
*
* @param id The ID of the resource to update.
* @param etag The entity tag value that is the expected value for the target
* resource. A value of null
will not set an
* etag precondition and a value of "*" will perform an
* unconditional update.
* @param attributesToUpdate The list of attributes (and their new values) to
* update on the resource. These attributes should
* conform to Section 3.2.2 of the SCIM 1.1
* specification (draft-scim-api-01),
* "Modifying Resources with PATCH".
* @param attributesToDelete The list of attributes to delete on the resource.
* @param requestedAttributes The attributes of updated resource to return.
* @return The updated resource returned by the service provider, or
* an empty resource with returned meta data if the
* {@code requestedAttributes} parameter was not specified and
* the service provider returned no content.
* @throws SCIMException If an error occurs.
*/
public R update(final String id, final String etag,
final List attributesToUpdate,
final List attributesToDelete,
final String... requestedAttributes)
throws SCIMException
{
URI uri =
UriBuilder.fromUri(scimService.getBaseURL()).path(
resourceDescriptor.getEndpoint()).path(id).build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
addAttributesQuery(clientResource, requestedAttributes);
if(scimService.getUserAgent() != null)
{
clientResource.header(HttpHeaders.USER_AGENT, scimService.getUserAgent());
}
if(etag != null && !etag.isEmpty())
{
clientResource.header(HttpHeaders.IF_MATCH, etag);
}
Diff diff = new Diff(resourceDescriptor, attributesToDelete,
attributesToUpdate);
final BaseResource resource =
diff.toPartialResource(resourceFactory, true);
StreamingOutput output = new StreamingOutput() {
public void write(final OutputStream outputStream)
throws IOException, WebApplicationException {
try {
marshaller.marshal(resource, outputStream);
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
}
}
};
ClientResponse response = null;
try
{
if(overrides[1])
{
clientResource.header("X-HTTP-Method-Override", "PATCH");
response = clientResource.post(output);
}
else
{
try
{
// WINK client doesn't have an invoke method where it always
// returns a ClientResponse like the other put, post, and get methods.
// This throws a ClientWebException if the server returns a non 200
// code.
response =
clientResource.invoke("PATCH", ClientResponse.class, output);
}
catch (ClientWebException e)
{
response = e.getResponse();
}
}
InputStream entity = response.getEntity(InputStream.class);
if(response.getStatusType() == Response.Status.OK)
{
R patchedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
resourceFactory);
addMissingMetaData(response, patchedResource);
return patchedResource;
}
else if (response.getStatusType() == Response.Status.NO_CONTENT)
{
R emptyResource =
resourceFactory.createResource(resourceDescriptor,
new SCIMObject());
emptyResource.setId(id);
addMissingMetaData(response, emptyResource);
return emptyResource;
}
else
{
throw createErrorResponseException(response, entity);
}
}
catch(SCIMException e)
{
throw e;
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Update the existing resource with the one provided (using the HTTP PATCH
* method). Note that if the {@code attributesToDelete} parameter is
* specified, those attributes will be removed from the resource before the
* {@code attributesToUpdate} are merged into the resource.
*
* @param id The ID of the resource to update.
* @param attributesToUpdate The list of attributes (and their new values) to
* update on the resource.
* @param attributesToDelete The list of attributes to delete on the resource.
* @throws SCIMException If an error occurs.
*/
@Deprecated
public void update(final String id,
final List attributesToUpdate,
final List attributesToDelete)
throws SCIMException
{
update(id, null, attributesToUpdate, attributesToDelete);
}
/**
* Delete the resource instance specified by the provided ID.
*
* @param id The ID of the resource to delete.
* @throws SCIMException If an error occurs.
*/
@Deprecated
public void delete(final String id)
throws SCIMException
{
delete(id, null);
}
/**
* Delete the specified resource instance.
*
* @param resource The resource to delete.
* @throws SCIMException If an error occurs.
*/
public void delete(final R resource)
throws SCIMException
{
delete(resource.getId(), resource.getMeta() == null ?
null : resource.getMeta().getVersion());
}
/**
* Delete the resource instance specified by the provided ID. This delete is
* conditional upon the provided entity tag matching the tag from the
* current resource. If (and only if) they match, the delete will be
* performed.
*
* @param id The ID of the resource to delete.
* @param etag The entity tag value that is the expected value for the target
* resource. A value of null
will not set an
* etag precondition and a value of "*" will perform an
* unconditional delete.
* @throws SCIMException If an error occurs.
*/
public void delete(final String id, final String etag)
throws SCIMException
{
URI uri =
UriBuilder.fromUri(scimService.getBaseURL()).path(
resourceDescriptor.getEndpoint()).path(id).build();
Resource clientResource =
client.resource(completeUri(uri));
if(!useUrlSuffix)
{
clientResource.accept(acceptType);
}
clientResource.contentType(contentType);
if(scimService.getUserAgent() != null)
{
clientResource.header(HttpHeaders.USER_AGENT, scimService.getUserAgent());
}
if(etag != null && !etag.isEmpty())
{
clientResource.header(HttpHeaders.IF_MATCH, etag);
}
ClientResponse response = null;
try
{
if(overrides[2])
{
clientResource.header("X-HTTP-Method-Override", "DELETE");
response = clientResource.post(null);
} else
{
response = clientResource.delete();
}
if(response.getStatusType() != Response.Status.OK)
{
InputStream entity = response.getEntity(InputStream.class);
throw createErrorResponseException(response, entity);
}
else
{
response.consumeContent();
}
}
catch(SCIMException e)
{
throw e;
}
catch(ClientRuntimeException cre)
{
// Treat SocketTimeoutException like HTTP 100
Throwable rootCause = StaticUtils.getRootCause(cre);
if (rootCause == null || !(rootCause instanceof SocketTimeoutException))
{
throw cre;
}
}
catch(Exception e)
{
throw SCIMException.createException(getStatusCode(e),
getExceptionMessage(e), e);
}
finally
{
if (response != null) {
response.close();
}
}
}
/**
* Add the attributes query parameter to the client resource request.
*
* @param clientResource The Wink client resource.
* @param requestedAttributes The SCIM attributes to request.
*/
private void addAttributesQuery(
final Resource clientResource,
final String... requestedAttributes)
{
if(requestedAttributes != null && requestedAttributes.length > 0)
{
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0; i < requestedAttributes.length; i++)
{
stringBuilder.append(requestedAttributes[i]);
if(i < requestedAttributes.length - 1)
{
stringBuilder.append(",");
}
}
clientResource.queryParam("attributes", stringBuilder.toString());
}
}
/**
* Add meta values from the response header to the meta complex attribute
* if they are missing.
*
* @param response The response from the service provider.
* @param resource The return resource instance.
*/
private void addMissingMetaData(final ClientResponse response,
final R resource)
{
URI headerLocation = null;
String headerEtag = null;
List values;
if(response.getStatusType() == Response.Status.CREATED ||
response.getStatusType().getFamily() ==
Response.Status.Family.REDIRECTION)
{
values = response.getHeaders().get(HttpHeaders.LOCATION);
}
else
{
values = response.getHeaders().get(HttpHeaders.CONTENT_LOCATION);
if(values == null || values.isEmpty())
{
// Fall back to using Location header to maintain backwards
// compatibility.
values = response.getHeaders().get(HttpHeaders.LOCATION);
}
}
if(values != null && !values.isEmpty())
{
headerLocation = URI.create(values.get(0));
}
values = response.getHeaders().get(HttpHeaders.ETAG);
if(values != null && !values.isEmpty())
{
headerEtag = values.get(0);
}
Meta meta = resource.getMeta();
if(meta == null)
{
meta = new Meta(null, null, null, null);
}
boolean modified = false;
if(headerLocation != null && meta.getLocation() == null)
{
meta.setLocation(headerLocation);
modified = true;
}
if(headerEtag != null && meta.getVersion() == null)
{
meta.setVersion(headerEtag);
modified = true;
}
if(modified)
{
resource.setMeta(meta);
}
}
/**
* Returns a SCIM exception representing the error response.
*
* @param response The client response.
* @param entity The response content.
*
* @return The SCIM exception representing the error response.
*/
private SCIMException createErrorResponseException(
final ClientResponse response,
final InputStream entity)
{
SCIMException scimException = null;
if(entity != null)
{
try
{
scimException = unmarshaller.unmarshalError(entity);
}
catch (InvalidResourceException e)
{
// The response content could not be parsed as a SCIM error
// response, which is the case if the response is a more general
// HTTP error. It is better to just provide the HTTP response
// details in this case.
Debug.debugException(e);
}
}
if(scimException == null)
{
scimException = SCIMException.createException(
response.getStatusCode(), response.getMessage());
}
if(response.getStatusType() == Response.Status.PRECONDITION_FAILED)
{
scimException = new PreconditionFailedException(
scimException.getMessage(),
response.getHeaders().getFirst(HttpHeaders.ETAG),
scimException.getCause());
}
else if(response.getStatusType() == Response.Status.NOT_MODIFIED)
{
scimException = new NotModifiedException(
scimException.getMessage(),
response.getHeaders().getFirst(HttpHeaders.ETAG),
scimException.getCause());
}
return scimException;
}
/**
* Returns the complete resource URI by appending the suffix if necessary.
*
* @param uri The URI to complete.
* @return The completed URI.
*/
private URI completeUri(final URI uri)
{
URI completedUri = uri;
if(useUrlSuffix)
{
try
{
if(acceptType == MediaType.APPLICATION_JSON_TYPE)
{
completedUri = new URI(uri.toString() + ".json");
}
else if(acceptType == MediaType.APPLICATION_XML_TYPE)
{
completedUri = new URI(uri.toString() + ".xml");
}
}
catch(URISyntaxException e)
{
throw new RuntimeException(e);
}
}
return completedUri;
}
/**
* Tries to deduce the most appropriate HTTP response code from the given
* exception. This method expects most exceptions to be one of 3 or 4
* expected runtime exceptions that are common to Wink and the Apache Http
* Client library.
*
* Note this method can return -1 for the special case of a
* {@link com.unboundid.scim.sdk.ConnectException}, in which the service
* provider could not be reached at all.
*
* @param t the Exception instance to analyze
* @return the most appropriate HTTP status code
*/
static int getStatusCode(final Throwable t)
{
Throwable rootCause = t;
if(rootCause instanceof ClientRuntimeException)
{
//Pull the underlying cause out of the ClientRuntimeException
rootCause = StaticUtils.getRootCause(t);
}
if(rootCause instanceof HttpResponseException)
{
HttpResponseException hre = (HttpResponseException) rootCause;
return hre.getStatusCode();
}
else if(rootCause instanceof HttpException)
{
if(rootCause instanceof RedirectException)
{
return 300;
}
else if(rootCause instanceof AuthenticationException)
{
return 401;
}
else if(rootCause instanceof MethodNotSupportedException)
{
return 501;
}
else if(rootCause instanceof UnsupportedHttpVersionException)
{
return 505;
}
}
else if(rootCause instanceof IOException)
{
if(rootCause instanceof NoHttpResponseException)
{
return 503;
}
else if(rootCause instanceof ConnectionClosedException)
{
return 503;
}
else
{
return -1;
}
}
if(t instanceof ClientWebException)
{
ClientWebException cwe = (ClientWebException) t;
return cwe.getResponse().getStatusCode();
}
else if(t instanceof ClientAuthenticationException)
{
return 401;
}
else if(t instanceof ClientConfigException)
{
return 400;
}
else
{
return 500;
}
}
/**
* Extracts the exception message from the root cause of the exception if
* possible.
*
* @param t the original Throwable that was caught. This may be null.
* @return the exception message from the root cause of the exception, or
* null if the specified Throwable is null or the message cannot be
* determined.
*/
static String getExceptionMessage(final Throwable t)
{
if(t == null)
{
return null;
}
Throwable rootCause = StaticUtils.getRootCause(t);
return rootCause.getMessage();
}
}