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}