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}