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