001/*
002 * Copyright 2011-2013 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim.sdk;
019
020import com.unboundid.scim.data.Meta;
021import com.unboundid.scim.data.ResourceFactory;
022import com.unboundid.scim.data.BaseResource;
023import com.unboundid.scim.marshal.Marshaller;
024import com.unboundid.scim.marshal.Unmarshaller;
025import com.unboundid.scim.marshal.json.JsonMarshaller;
026import com.unboundid.scim.marshal.json.JsonUnmarshaller;
027import com.unboundid.scim.marshal.xml.XmlMarshaller;
028import com.unboundid.scim.marshal.xml.XmlUnmarshaller;
029import com.unboundid.scim.schema.ResourceDescriptor;
030
031import org.apache.http.ConnectionClosedException;
032import org.apache.http.HttpException;
033import org.apache.http.MethodNotSupportedException;
034import org.apache.http.NoHttpResponseException;
035import org.apache.http.UnsupportedHttpVersionException;
036import org.apache.http.auth.AuthenticationException;
037import org.apache.http.client.HttpResponseException;
038import org.apache.http.client.RedirectException;
039import org.apache.http.conn.ConnectTimeoutException;
040import org.apache.wink.client.ClientAuthenticationException;
041import org.apache.wink.client.ClientConfigException;
042import org.apache.wink.client.ClientResponse;
043import org.apache.wink.client.ClientRuntimeException;
044import org.apache.wink.client.ClientWebException;
045import org.apache.wink.client.RestClient;
046
047import javax.ws.rs.WebApplicationException;
048import javax.ws.rs.core.MediaType;
049import javax.ws.rs.core.Response;
050import javax.ws.rs.core.StreamingOutput;
051import javax.ws.rs.core.UriBuilder;
052import java.io.IOException;
053import java.io.InputStream;
054import java.io.OutputStream;
055import java.net.ConnectException;
056import java.net.URI;
057import java.util.List;
058import java.util.Map;
059
060
061/**
062 * This class represents a SCIM endpoint (ie. Users, Groups, etc.) and handles
063 * all protocol-level interactions with the service provider. It acts as a
064 * helper class for invoking CRUD operations of resources and processing their
065 * results.
066 *
067 * @param <R> The type of resource instances handled by this SCIMEndpoint.
068 */
069public class SCIMEndpoint<R extends BaseResource>
070{
071  private final SCIMService scimService;
072  private final ResourceDescriptor resourceDescriptor;
073  private final ResourceFactory<R> resourceFactory;
074  private final Unmarshaller unmarshaller;
075  private final Marshaller marshaller;
076  private final MediaType contentType;
077  private final MediaType acceptType;
078  private final boolean[] overrides = new boolean[3];
079  private final RestClient client;
080
081
082  /**
083   * Create a SCIMEndpoint with the provided information.
084   *
085   * @param scimService The SCIMService to use.
086   * @param restClient The Wink REST client.
087   * @param resourceDescriptor The resource descriptor of this endpoint.
088   * @param resourceFactory The ResourceFactory that should be used to create
089   *                        resource instances.
090   */
091  SCIMEndpoint(final SCIMService scimService,
092               final RestClient restClient,
093               final ResourceDescriptor resourceDescriptor,
094               final ResourceFactory<R> resourceFactory)
095  {
096    this.scimService = scimService;
097    this.client = restClient;
098    this.resourceDescriptor = resourceDescriptor;
099    this.resourceFactory = resourceFactory;
100    this.contentType = scimService.getContentType();
101    this.acceptType = scimService.getAcceptType();
102    this.overrides[0] = scimService.isOverridePut();
103    this.overrides[1] = scimService.isOverridePatch();
104    this.overrides[2] = scimService.isOverrideDelete();
105
106    if (scimService.getContentType().equals(MediaType.APPLICATION_JSON_TYPE))
107    {
108      this.marshaller = new JsonMarshaller();
109    }
110    else
111    {
112      this.marshaller = new XmlMarshaller();
113    }
114
115    if(scimService.getAcceptType().equals(MediaType.APPLICATION_JSON_TYPE))
116    {
117      this.unmarshaller = new JsonUnmarshaller();
118    }
119    else
120    {
121      this.unmarshaller = new XmlUnmarshaller();
122    }
123  }
124
125
126
127  /**
128   * Constructs a new instance of a resource object which is empty. This
129   * method does not interact with the SCIM service. It creates a local object
130   * that may be provided to the {@link SCIMEndpoint#create} method after the
131   * attributes have been specified.
132   *
133   * @return  A new instance of a resource object.
134   */
135  public R newResource()
136  {
137    return resourceFactory.createResource(resourceDescriptor, new SCIMObject());
138  }
139
140  /**
141   * Retrieves a resource instance given the ID.
142   *
143   * @param id The ID of the resource to retrieve.
144   * @return The retrieved resource.
145   * @throws SCIMException If an error occurs.
146   */
147  public R get(final String id)
148      throws SCIMException
149  {
150    return get(id, null, null);
151  }
152
153  /**
154   * Retrieves a resource instance given the ID, only if the current version
155   * has been modified.
156   *
157   * @param id The ID of the resource to retrieve.
158   * @param etag The entity tag that indicates the entry should be returned
159   *             only if the entity tag of the current resource is different
160   *             from the provided value. A value of <code>null</code> indicates
161   *             unconditional return.
162   * @param requestedAttributes The attributes of the resource to retrieve.
163   * @return The retrieved resource or <code>null</code> if the requested
164   * resource has not been modified.
165   * @throws SCIMException If an error occurs.
166   */
167  public R get(final String id, final String etag,
168               final String... requestedAttributes)
169      throws SCIMException
170  {
171    final UriBuilder uriBuilder = UriBuilder.fromUri(scimService.getBaseURL());
172    uriBuilder.path(resourceDescriptor.getEndpoint());
173
174    // The ServiceProviderConfig is a special case where the id is not
175    // specified.
176    if (id != null)
177    {
178      uriBuilder.path(id);
179    }
180
181    URI uri = uriBuilder.build();
182    org.apache.wink.client.Resource clientResource = client.resource(uri);
183    clientResource.accept(acceptType);
184    clientResource.contentType(contentType);
185    addAttributesQuery(clientResource, requestedAttributes);
186
187    if(scimService.getUserAgent() != null)
188    {
189      clientResource.header("User-Agent", scimService.getUserAgent());
190    }
191
192    if(etag != null && !etag.isEmpty())
193    {
194      clientResource.header("If-None-Match", etag);
195    }
196
197    InputStream entity = null;
198    try
199    {
200      ClientResponse response = clientResource.get();
201      entity = response.getEntity(InputStream.class);
202
203      if(response.getStatusType() == Response.Status.NOT_MODIFIED)
204      {
205        return null;
206      }
207      else if(response.getStatusType() == Response.Status.OK)
208      {
209        R resource = unmarshaller.unmarshal(entity, resourceDescriptor,
210            resourceFactory);
211        addMissingMetaData(response, resource);
212        return resource;
213      }
214      else
215      {
216        throw createErrorResponseException(response, entity);
217      }
218    }
219    catch(SCIMException e)
220    {
221      throw e;
222    }
223    catch(Exception e)
224    {
225      throw SCIMException.createException(getStatusCode(e),
226                                          getExceptionMessage(e), e);
227    }
228    finally
229    {
230      try {
231        if (entity != null) {
232          entity.close();
233        }
234      } catch (IOException e) {
235        // Lets just log this and ignore.
236        Debug.debugException(e);
237      }
238    }
239  }
240
241  /**
242   * Retrieves all resource instances that match the provided filter.
243   *
244   * @param filter The filter that should be used.
245   * @return The resource instances that match the provided filter.
246   * @throws SCIMException If an error occurs.
247   */
248  public Resources<R> query(final String filter)
249      throws SCIMException
250  {
251    return query(filter, null, null, null);
252  }
253
254  /**
255   * Retrieves all resource instances that match the provided filter.
256   * Matching resources are returned sorted according to the provided
257   * SortParameters. PageParameters maybe used to specify the range of
258   * resource instances that are returned.
259   *
260   * @param filter The filter that should be used.
261   * @param sortParameters The sort parameters that should be used.
262   * @param pageParameters The page parameters that should be used.
263   * @param requestedAttributes The attributes of the resource to retrieve.
264   * @return The resource instances that match the provided filter.
265   * @throws SCIMException If an error occurs.
266   */
267  public Resources<R> query(final String filter,
268                            final SortParameters sortParameters,
269                            final PageParameters pageParameters,
270                            final String... requestedAttributes)
271      throws SCIMException
272  {
273    return query(filter, sortParameters, pageParameters,
274                    null, requestedAttributes);
275  }
276
277  /**
278   * Retrieves all resource instances that match the provided filter.
279   * Matching resources are returned sorted according to the provided
280   * SortParameters. PageParameters maybe used to specify the range of
281   * resource instances that are returned. Additional query parameters may
282   * be specified using a Map of parameter names to their values.
283   *
284   * @param filter The filter that should be used.
285   * @param sortParameters The sort parameters that should be used.
286   * @param pageParameters The page parameters that should be used.
287   * @param additionalQueryParams A map of additional query parameters that
288   *                              should be included.
289   * @param requestedAttributes The attributes of the resource to retrieve.
290   * @return The resource instances that match the provided filter.
291   * @throws SCIMException If an error occurs.
292   */
293  public Resources<R> query(final String filter,
294                            final SortParameters sortParameters,
295                            final PageParameters pageParameters,
296                            final Map<String,String> additionalQueryParams,
297                            final String... requestedAttributes)
298      throws SCIMException
299  {
300    URI uri =
301        UriBuilder.fromUri(scimService.getBaseURL()).path(
302            resourceDescriptor.getEndpoint()).build();
303    org.apache.wink.client.Resource clientResource = client.resource(uri);
304    clientResource.accept(acceptType);
305    clientResource.contentType(contentType);
306    addAttributesQuery(clientResource, requestedAttributes);
307    if(scimService.getUserAgent() != null)
308    {
309      clientResource.header("User-Agent", scimService.getUserAgent());
310    }
311    if(filter != null)
312    {
313      clientResource.queryParam("filter", filter);
314    }
315    if(sortParameters != null)
316    {
317      clientResource.queryParam("sortBy",
318          sortParameters.getSortBy().toString());
319      if(!sortParameters.isAscendingOrder())
320      {
321        clientResource.queryParam("sortOrder", sortParameters.getSortOrder());
322      }
323    }
324    if(pageParameters != null)
325    {
326      clientResource.queryParam("startIndex",
327          String.valueOf(pageParameters.getStartIndex()));
328      if (pageParameters.getCount() > 0)
329      {
330        clientResource.queryParam("count",
331                                  String.valueOf(pageParameters.getCount()));
332      }
333    }
334    if(additionalQueryParams != null)
335    {
336      for (String key : additionalQueryParams.keySet())
337      {
338        clientResource.queryParam(key, additionalQueryParams.get(key));
339      }
340    }
341
342    InputStream entity = null;
343    try
344    {
345      ClientResponse response = clientResource.get();
346      entity = response.getEntity(InputStream.class);
347
348      if(response.getStatusType() == Response.Status.OK)
349      {
350        return unmarshaller.unmarshalResources(entity, resourceDescriptor,
351            resourceFactory);
352      }
353      else
354      {
355        throw createErrorResponseException(response, entity);
356      }
357    }
358    catch(SCIMException e)
359    {
360      throw e;
361    }
362    catch(Exception e)
363    {
364      throw SCIMException.createException(getStatusCode(e),
365                                          getExceptionMessage(e), e);
366    }
367    finally
368    {
369      try {
370        if (entity != null) {
371          entity.close();
372        }
373      } catch (IOException e) {
374        // Lets just log this and ignore.
375        Debug.debugException(e);
376      }
377    }
378  }
379
380
381
382  /**
383   * Create the specified resource instance at the service provider.
384   *
385   * @param resource The resource to create.
386   * @return The newly inserted resource returned by the service provider.
387   * @throws SCIMException If an error occurs.
388   */
389  public R create(final R resource) throws SCIMException
390  {
391    return create(resource, null);
392  }
393
394  /**
395   * Create the specified resource instance at the service provider and return
396   * only the specified attributes from the newly inserted resource.
397   *
398   * @param resource The resource to create.
399   * @param requestedAttributes The attributes of the newly inserted resource
400   *                            to retrieve.
401   * @return The newly inserted resource returned by the service provider.
402   * @throws SCIMException If an error occurs.
403   */
404  public R create(final R resource,
405                  final String... requestedAttributes)
406      throws SCIMException
407  {
408
409    URI uri =
410        UriBuilder.fromUri(scimService.getBaseURL()).path(
411            resourceDescriptor.getEndpoint()).build();
412    org.apache.wink.client.Resource clientResource = client.resource(uri);
413    clientResource.accept(acceptType);
414    clientResource.contentType(contentType);
415    addAttributesQuery(clientResource, requestedAttributes);
416    if(scimService.getUserAgent() != null)
417    {
418      clientResource.header("User-Agent", scimService.getUserAgent());
419    }
420
421    StreamingOutput output = new StreamingOutput() {
422      public void write(final OutputStream outputStream)
423          throws IOException, WebApplicationException {
424        try {
425          marshaller.marshal(resource, outputStream);
426        } catch (Exception e) {
427          throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
428        }
429      }
430    };
431
432
433    InputStream entity = null;
434    try
435    {
436      ClientResponse response = clientResource.post(output);
437      entity = response.getEntity(InputStream.class);
438
439      if(response.getStatusType() == Response.Status.CREATED)
440      {
441        R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
442            resourceFactory);
443        addMissingMetaData(response, postedResource);
444        return postedResource;
445      }
446      else
447      {
448        throw createErrorResponseException(response, entity);
449      }
450    }
451    catch(SCIMException e)
452    {
453      throw e;
454    }
455    catch(Exception e)
456    {
457      throw SCIMException.createException(getStatusCode(e),
458                                          getExceptionMessage(e), e);
459    }
460    finally
461    {
462      try {
463        if (entity != null) {
464          entity.close();
465        }
466      } catch (IOException e) {
467        // Lets just log this and ignore.
468        Debug.debugException(e);
469      }
470    }
471  }
472
473  /**
474   * Update the existing resource with the one provided (using the HTTP PUT
475   * method).
476   *
477   * @param resource The modified resource to be updated.
478   * @return The updated resource returned by the service provider.
479   * @throws SCIMException If an error occurs.
480   */
481  public R update(final R resource)
482      throws SCIMException
483  {
484    return update(resource, null, null);
485  }
486
487  /**
488   * Update the existing resource with the one provided (using the HTTP PUT
489   * method). This update is conditional upon the provided entity tag matching
490   * the tag from the current resource. If (and only if) they match, the update
491   * will be performed.
492   *
493   * @param resource The modified resource to be updated.
494   * @param etag The entity tag value that is the expected value for the target
495   *             resource. A value of <code>null</code> will not set an
496   *             etag precondition and a value of "*" will perform an
497   *             unconditional update.
498   * @param requestedAttributes The attributes of updated resource
499   *                            to return.
500   * @return The updated resource returned by the service provider.
501   * @throws SCIMException If an error occurs.
502   */
503  public R update(final R resource, final String etag,
504                  final String... requestedAttributes)
505      throws SCIMException
506  {
507    String id = resource.getId();
508    if(id == null)
509    {
510      throw new InvalidResourceException("Resource must have a valid ID");
511    }
512    URI uri =
513        UriBuilder.fromUri(scimService.getBaseURL()).path(
514            resourceDescriptor.getEndpoint()).path(id).build();
515    org.apache.wink.client.Resource clientResource = client.resource(uri);
516    clientResource.accept(acceptType);
517    clientResource.contentType(contentType);
518    addAttributesQuery(clientResource, requestedAttributes);
519    if(scimService.getUserAgent() != null)
520    {
521      clientResource.header("User-Agent", scimService.getUserAgent());
522    }
523    if(etag != null && !etag.isEmpty())
524    {
525      clientResource.header("If-Match", etag);
526    }
527
528    StreamingOutput output = new StreamingOutput() {
529      public void write(final OutputStream outputStream)
530          throws IOException, WebApplicationException {
531        try {
532          marshaller.marshal(resource, outputStream);
533        } catch (Exception e) {
534          throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
535        }
536      }
537    };
538
539    InputStream entity = null;
540    try
541    {
542      ClientResponse response;
543      if(overrides[0])
544      {
545        clientResource.header("X-HTTP-Method-Override", "PUT");
546        response = clientResource.post(output);
547      }
548      else
549      {
550        response = clientResource.put(output);
551      }
552
553      entity = response.getEntity(InputStream.class);
554
555      if(response.getStatusType() == Response.Status.OK)
556      {
557        R postedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
558            resourceFactory);
559        addMissingMetaData(response, postedResource);
560        return postedResource;
561      }
562      else
563      {
564        throw createErrorResponseException(response, entity);
565      }
566    }
567    catch(SCIMException e)
568    {
569      throw e;
570    }
571    catch(Exception e)
572    {
573      throw SCIMException.createException(getStatusCode(e),
574                                          getExceptionMessage(e), e);
575    }
576    finally
577    {
578      try {
579        if (entity != null) {
580          entity.close();
581        }
582      } catch (IOException e) {
583        // Lets just log this and ignore.
584        Debug.debugException(e);
585      }
586    }
587  }
588
589  /**
590   * Update the existing resource with the one provided (using the HTTP PATCH
591   * method). Note that if the {@code attributesToDelete} parameter is
592   * specified, those attributes will be removed from the resource before the
593   * {@code attributesToUpdate} are merged into the resource.
594   *
595   * @param id The ID of the resource to update.
596   * @param etag The entity tag value that is the expected value for the target
597   *             resource. A value of <code>null</code> will not set an
598   *             etag precondition and a value of "*" will perform an
599   *             unconditional update.
600   * @param attributesToUpdate The list of attributes (and their new values) to
601   *                           update on the resource. These attributes should
602   *                           conform to Section 3.2.2 of the SCIM 1.1
603   *                           specification (<i>draft-scim-api-01</i>),
604   *                           "Modifying Resources with PATCH".
605   * @param attributesToDelete The list of attributes to delete on the resource.
606   * @param requestedAttributes The attributes of updated resource to return.
607   * @return The updated resource returned by the service provider, or
608   *         {@code null} if the {@code requestedAttributes} parameter was not
609   *         specified.
610   * @throws SCIMException If an error occurs.
611   */
612  public R update(final String id, final String etag,
613                  final List<SCIMAttribute> attributesToUpdate,
614                  final List<String> attributesToDelete,
615                  final String... requestedAttributes)
616          throws SCIMException
617  {
618    if(id == null)
619    {
620      throw new InvalidResourceException("Resource must have a valid ID");
621    }
622    URI uri =
623            UriBuilder.fromUri(scimService.getBaseURL()).path(
624                    resourceDescriptor.getEndpoint()).path(id).build();
625    org.apache.wink.client.Resource clientResource = client.resource(uri);
626    clientResource.accept(acceptType);
627    clientResource.contentType(contentType);
628    addAttributesQuery(clientResource, requestedAttributes);
629
630    if(scimService.getUserAgent() != null)
631    {
632      clientResource.header("User-Agent", scimService.getUserAgent());
633    }
634    if(etag != null && !etag.isEmpty())
635    {
636      clientResource.header("If-Match", etag);
637    }
638
639    Diff<R> diff = new Diff<R>(resourceDescriptor, attributesToDelete,
640                               attributesToUpdate);
641    final BaseResource resource =
642            diff.toPartialResource(resourceFactory, true);
643
644    StreamingOutput output = new StreamingOutput() {
645      public void write(final OutputStream outputStream)
646              throws IOException, WebApplicationException {
647        try {
648          marshaller.marshal(resource, outputStream);
649        } catch (Exception e) {
650          throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
651        }
652      }
653    };
654
655    InputStream entity = null;
656    try
657    {
658      ClientResponse response;
659      if(overrides[1])
660      {
661        clientResource.header("X-HTTP-Method-Override", "PATCH");
662        response = clientResource.post(output);
663      }
664      else
665      {
666        try
667        {
668          // WINK client doesn't have an invoke method where it always
669          // returns a ClientResponse like the other put, post, and get methods.
670          // This throws a ClientWebException if the server returns a non 200
671          // code.
672          response =
673              clientResource.invoke("PATCH", ClientResponse.class, output);
674        }
675        catch (ClientWebException e)
676        {
677          response = e.getResponse();
678        }
679      }
680
681      entity = response.getEntity(InputStream.class);
682
683      if(response.getStatusType() == Response.Status.OK)
684      {
685        R patchedResource = unmarshaller.unmarshal(entity, resourceDescriptor,
686                resourceFactory);
687        addMissingMetaData(response, patchedResource);
688        return patchedResource;
689      }
690      else if (response.getStatusType() == Response.Status.NO_CONTENT)
691      {
692         return null;
693      }
694      else
695      {
696        throw createErrorResponseException(response, entity);
697      }
698    }
699    catch(SCIMException e)
700    {
701      throw e;
702    }
703    catch(Exception e)
704    {
705      throw SCIMException.createException(getStatusCode(e),
706                                          getExceptionMessage(e), e);
707    }
708    finally
709    {
710      try {
711        if (entity != null) {
712          entity.close();
713        }
714      } catch (IOException e) {
715        Debug.debugException(e);
716      }
717    }
718  }
719
720  /**
721   * Update the existing resource with the one provided (using the HTTP PATCH
722   * method). Note that if the {@code attributesToDelete} parameter is
723   * specified, those attributes will be removed from the resource before the
724   * {@code attributesToUpdate} are merged into the resource.
725   *
726   * @param id The ID of the resource to update.
727   * @param attributesToUpdate The list of attributes (and their new values) to
728   *                           update on the resource.
729   * @param attributesToDelete The list of attributes to delete on the resource.
730   * @throws SCIMException If an error occurs.
731   */
732  public void update(final String id,
733                     final List<SCIMAttribute> attributesToUpdate,
734                     final List<String> attributesToDelete)
735           throws SCIMException
736  {
737    update(id, null, attributesToUpdate, attributesToDelete);
738  }
739
740  /**
741   * Delete the resource instance specified by the provided ID.
742   *
743   * @param id The ID of the resource to delete.
744   * @throws SCIMException If an error occurs.
745   */
746  public void delete(final String id)
747      throws SCIMException
748  {
749    delete(id, null);
750  }
751
752  /**
753   * Delete the resource instance specified by the provided ID. This delete is
754   * conditional upon the provided entity tag matching the tag from the
755   * current resource. If (and only if) they match, the delete will be
756   * performed.
757   *
758   * @param id The ID of the resource to delete.
759   * @param etag The entity tag value that is the expected value for the target
760   *             resource. A value of <code>null</code> will not set an
761   *             etag precondition and a value of "*" will perform an
762   *             unconditional delete.
763   * @throws SCIMException If an error occurs.
764   */
765  public void delete(final String id, final String etag)
766      throws SCIMException
767  {
768    URI uri =
769        UriBuilder.fromUri(scimService.getBaseURL()).path(
770            resourceDescriptor.getEndpoint()).path(id).build();
771    org.apache.wink.client.Resource clientResource = client.resource(uri);
772    clientResource.accept(acceptType);
773    clientResource.contentType(contentType);
774    if(scimService.getUserAgent() != null)
775    {
776      clientResource.header("User-Agent", scimService.getUserAgent());
777    }
778    if(etag != null && !etag.isEmpty())
779    {
780      clientResource.header("If-Match", etag);
781    }
782
783
784    InputStream entity = null;
785    try
786    {
787      ClientResponse response;
788      if(overrides[2])
789      {
790        clientResource.header("X-HTTP-Method-Override", "DELETE");
791        response = clientResource.post(null);
792      }
793      else
794      {
795        response = clientResource.delete();
796      }
797
798      entity = response.getEntity(InputStream.class);
799
800      if(response.getStatusType() != Response.Status.OK)
801      {
802        throw createErrorResponseException(response, entity);
803      }
804    }
805    catch(SCIMException e)
806    {
807      throw e;
808    }
809    catch(Exception e)
810    {
811      throw SCIMException.createException(getStatusCode(e),
812                                          getExceptionMessage(e), e);
813    }
814    finally
815    {
816      try {
817        if (entity != null) {
818          entity.close();
819        }
820      } catch (IOException e) {
821        // Lets just log this and ignore.
822        Debug.debugException(e);
823      }
824    }
825  }
826
827  /**
828   * Add the attributes query parameter to the client resource request.
829   *
830   * @param clientResource The Wink client resource.
831   * @param requestedAttributes The SCIM attributes to request.
832   */
833  private void addAttributesQuery(
834      final org.apache.wink.client.Resource clientResource,
835      final String... requestedAttributes)
836  {
837    if(requestedAttributes != null && requestedAttributes.length > 0)
838    {
839      StringBuilder stringBuilder = new StringBuilder();
840      for(int i = 0; i < requestedAttributes.length; i++)
841      {
842        stringBuilder.append(requestedAttributes[i]);
843        if(i < requestedAttributes.length - 1)
844        {
845          stringBuilder.append(",");
846        }
847      }
848      clientResource.queryParam("attributes", stringBuilder.toString());
849    }
850  }
851
852  /**
853   * Add meta values from the response header to the meta complex attribute
854   * if they are missing.
855   *
856   * @param response The response from the service provider.
857   * @param resource The return resource instance.
858   */
859  private void addMissingMetaData(final ClientResponse response,
860                                  final R resource)
861  {
862    URI headerLocation = null;
863    String headerEtag = null;
864    List<String> values = response.getHeaders().get("Location");
865    if(values != null && !values.isEmpty())
866    {
867      headerLocation = URI.create(values.get(0));
868    }
869    values = response.getHeaders().get("Etag");
870    if(values != null && !values.isEmpty())
871    {
872      headerEtag = values.get(0);
873    }
874    Meta meta = resource.getMeta();
875    if(meta == null)
876    {
877      meta = new Meta(null, null, null, null);
878    }
879    boolean modified = false;
880    if(headerLocation != null && meta.getLocation() == null)
881    {
882      meta.setLocation(headerLocation);
883      modified = true;
884    }
885    if(headerEtag != null && meta.getVersion() == null)
886    {
887      meta.setVersion(headerEtag);
888      modified = true;
889    }
890    if(modified)
891    {
892      resource.setMeta(meta);
893    }
894  }
895
896
897
898  /**
899   * Returns a SCIM exception representing the error response.
900   *
901   * @param response  The client response.
902   * @param entity    The response content.
903   *
904   * @return  The SCIM exception representing the error response.
905   */
906  private SCIMException createErrorResponseException(
907      final ClientResponse response,
908      final InputStream entity)
909  {
910    SCIMException scimException = null;
911
912    if(entity != null)
913    {
914      try
915      {
916        scimException = unmarshaller.unmarshalError(entity);
917      }
918      catch (InvalidResourceException e)
919      {
920        // The response content could not be parsed as a SCIM error
921        // response, which is the case if the response is a more general
922        // HTTP error. It is better to just provide the HTTP response
923        // details in this case.
924        Debug.debugException(e);
925      }
926    }
927
928    if(scimException == null)
929    {
930      scimException = SCIMException.createException(
931          response.getStatusCode(), response.getMessage());
932    }
933
934    return scimException;
935  }
936
937  /**
938   * Tries to deduce the most appropriate HTTP response code from the given
939   * exception. This method expects most exceptions to be one of 3 or 4
940   * expected runtime exceptions that are common to Wink and the Apache Http
941   * Client library.
942   * <p>
943   * Note this method can return -1 for the special case of a
944   * {@link com.unboundid.scim.sdk.ConnectException}, in which the service
945   * provider could not be reached at all.
946   *
947   * @param t the Exception instance to analyze
948   * @return the most appropriate HTTP status code
949   */
950  static int getStatusCode(final Throwable t)
951  {
952    Throwable rootCause = t;
953    if(rootCause instanceof ClientRuntimeException)
954    {
955      //Pull the underlying cause out of the ClientRuntimeException
956      rootCause = StaticUtils.getRootCause(t);
957    }
958
959    if(rootCause instanceof HttpResponseException)
960    {
961      HttpResponseException hre = (HttpResponseException) rootCause;
962      return hre.getStatusCode();
963    }
964    else if(rootCause instanceof HttpException)
965    {
966      if(rootCause instanceof RedirectException)
967      {
968        return 300;
969      }
970      else if(rootCause instanceof AuthenticationException)
971      {
972        return 401;
973      }
974      else if(rootCause instanceof MethodNotSupportedException)
975      {
976        return 501;
977      }
978      else if(rootCause instanceof UnsupportedHttpVersionException)
979      {
980        return 505;
981      }
982    }
983    else if(rootCause instanceof IOException)
984    {
985      if(rootCause instanceof ConnectException)
986      {
987        return -1;
988      }
989      else if(rootCause instanceof ConnectTimeoutException)
990      {
991        return -1;
992      }
993      else if(rootCause instanceof NoHttpResponseException)
994      {
995        return 503;
996      }
997      else if(rootCause instanceof ConnectionClosedException)
998      {
999        return 503;
1000      }
1001      else
1002      {
1003        return -1;
1004      }
1005    }
1006
1007    if(t instanceof ClientWebException)
1008    {
1009      ClientWebException cwe = (ClientWebException) t;
1010      return cwe.getResponse().getStatusCode();
1011    }
1012    else if(t instanceof ClientAuthenticationException)
1013    {
1014      return 401;
1015    }
1016    else if(t instanceof ClientConfigException)
1017    {
1018      return 400;
1019    }
1020    else
1021    {
1022      return 500;
1023    }
1024  }
1025
1026  /**
1027   * Extracts the exception message from the root cause of the exception if
1028   * possible.
1029   *
1030   * @param t the original Throwable that was caught. This may be null.
1031   * @return the exception message from the root cause of the exception, or
1032   *         null if the specified Throwable is null or the message cannot be
1033   *         determined.
1034   */
1035  static String getExceptionMessage(final Throwable t)
1036  {
1037    if(t == null)
1038    {
1039      return null;
1040    }
1041
1042    Throwable rootCause = StaticUtils.getRootCause(t);
1043    return rootCause.getMessage();
1044  }
1045}