001/* 002 * Copyright 2012-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.wink; 019 020import com.unboundid.scim.data.BaseResource; 021import com.unboundid.scim.schema.ResourceDescriptor; 022import com.unboundid.scim.sdk.BulkContentHandler; 023import com.unboundid.scim.sdk.BulkException; 024import com.unboundid.scim.sdk.BulkOperation; 025import com.unboundid.scim.sdk.BulkOperation.Method; 026import com.unboundid.scim.sdk.BulkStreamResponse; 027import com.unboundid.scim.sdk.Debug; 028import com.unboundid.scim.sdk.DeleteResourceRequest; 029import com.unboundid.scim.sdk.InvalidResourceException; 030import com.unboundid.scim.sdk.OAuthTokenHandler; 031import com.unboundid.scim.sdk.PatchResourceRequest; 032import com.unboundid.scim.sdk.PostResourceRequest; 033import com.unboundid.scim.sdk.PreconditionFailedException; 034import com.unboundid.scim.sdk.PutResourceRequest; 035import com.unboundid.scim.sdk.SCIMAttribute; 036import com.unboundid.scim.sdk.SCIMAttributeValue; 037import com.unboundid.scim.sdk.SCIMBackend; 038import com.unboundid.scim.sdk.SCIMException; 039import com.unboundid.scim.sdk.SCIMObject; 040import com.unboundid.scim.sdk.SCIMQueryAttributes; 041import com.unboundid.scim.sdk.ServerErrorException; 042import com.unboundid.scim.sdk.Status; 043import com.unboundid.scim.sdk.UnauthorizedException; 044 045import static com.unboundid.scim.wink.AbstractSCIMResource.validateOAuthToken; 046 047import javax.ws.rs.core.MediaType; 048import javax.ws.rs.core.Response; 049import javax.ws.rs.core.UriBuilder; 050import java.util.ArrayList; 051import java.util.Arrays; 052import java.util.Collection; 053import java.util.HashMap; 054import java.util.HashSet; 055import java.util.Map; 056import java.util.Set; 057import java.util.concurrent.atomic.AtomicReference; 058 059 060/** 061 * This class implements the bulk operation handler to process bulk operation 062 * requests in the SCIM server. 063 * The original purpose of the BulkContentHandler interface was to allow 064 * each operation to be processed as soon as it had been read so that 065 * we do not have to hold the entire content of a bulk request in 066 * memory. However, there are two issues making that approach infeasible: 067 * <ol> 068 * <li>Since JSON objects are unordered, the failOnErrors value could 069 * conceivably come after the Operations array, which would be too late.</li> 070 * <li>It would not be possible to reject a request that exceeded the 071 * maxOperations setting without processing any operations.</li> 072 * </ol> 073 */ 074public class BulkContentRequestHandler extends BulkContentHandler 075{ 076 /** 077 * The SCIM application. 078 */ 079 private final SCIMApplication application; 080 081 /** 082 * The request context for the bulk request. 083 */ 084 private final RequestContext requestContext; 085 086 /** 087 * The SCIM backend to process the operations. 088 */ 089 private final SCIMBackend backend; 090 091 /** 092 * The OAuth 2.0 bearer token handler. This may be null. 093 */ 094 private final OAuthTokenHandler tokenHandler; 095 096 /** 097 * The bulk stream response to write the operation responses to. 098 */ 099 private final BulkStreamResponse bulkStreamResponse; 100 101 /** 102 * A map from bulkId to resourceID. 103 */ 104 private final Map<String,String> resourceIDs; 105 106 /** 107 * A set containing the unresolved bulkId data references for the latest 108 * bulk operation. 109 */ 110 private final Set<String> unresolvedBulkIdRefs; 111 112 113 private int errorCount = 0; 114 115 /** 116 * The set of defined bulkIds from all operations. 117 */ 118 private final Set<String> bulkIds; 119 120 121 122 /** 123 * The number of errors that the Service Provider will accept before the 124 * operation is terminated and an error response is returned. The default 125 * is to continue performing as many changes as possible without regard to 126 * failures. 127 */ 128 private int failOnErrors = Integer.MAX_VALUE; 129 130 131 132 /** 133 * Create a new instance of this bulk operation handler. 134 * 135 * @param application The SCIM application. 136 * @param requestContext The request context for the bulk request. 137 * @param backend The SCIM backend to process the operations. 138 * @param bulkStreamResponse The bulk stream response to write response 139 * operations to. 140 * @param tokenHandler The OAuth token handler implementation to use. 141 */ 142 public BulkContentRequestHandler( 143 final SCIMApplication application, 144 final RequestContext requestContext, 145 final SCIMBackend backend, 146 final BulkStreamResponse bulkStreamResponse, 147 final OAuthTokenHandler tokenHandler) 148 { 149 this.application = application; 150 this.requestContext = requestContext; 151 this.backend = backend; 152 this.tokenHandler = tokenHandler; 153 this.bulkStreamResponse = bulkStreamResponse; 154 155 resourceIDs = new HashMap<String, String>(); 156 unresolvedBulkIdRefs = new HashSet<String>(); 157 bulkIds = new HashSet<String>(); 158 } 159 160 161 162 /** 163 * {@inheritDoc} 164 */ 165 public void handleFailOnErrors(final int failOnErrors) 166 { 167 this.failOnErrors = failOnErrors; 168 } 169 170 171 172 /** 173 * {@inheritDoc} 174 */ 175 @Override 176 public String transformValue(final int opIndex, final String value) 177 { 178 if (value.startsWith("bulkId:")) 179 { 180 final String bulkId = value.substring(7); 181 final String resourceID = resourceIDs.get(bulkId); 182 if (resourceID != null) 183 { 184 return resourceID; 185 } 186 else 187 { 188 unresolvedBulkIdRefs.add(bulkId); 189 } 190 } 191 192 return value; 193 } 194 195 196 197 /** 198 * {@inheritDoc} 199 */ 200 @Override 201 public ResourceDescriptor getResourceDescriptor(final String endpoint) 202 { 203 return backend.getResourceDescriptor(endpoint); 204 } 205 206 207 208 /** 209 * {@inheritDoc} 210 */ 211 public void handleOperation(final int opIndex, 212 final BulkOperation bulkOperation) 213 throws BulkException, SCIMException 214 { 215 if (errorCount < failOnErrors) 216 { 217 final BulkOperation response = processOperation(bulkOperation); 218 unresolvedBulkIdRefs.clear(); 219 bulkStreamResponse.writeBulkOperation(response); 220 } 221 } 222 223 224 225 /** 226 * {@inheritDoc} 227 */ 228 public boolean handleException(final int opIndex, 229 final BulkException bulkException) 230 throws SCIMException 231 { 232 Debug.debugException(bulkException); 233 if (errorCount < failOnErrors) 234 { 235 int statusCode = bulkException.getCause().getStatusCode(); 236 String statusMessage = bulkException.getCause().getMessage(); 237 238 final Status status = 239 new Status(String.valueOf(statusCode), statusMessage); 240 241 final Method method = bulkException.getMethod(); 242 243 // The bulk exception contains the path from the request. We just 244 // need to prepend the URL base. 245 String location = null; 246 if (method != BulkOperation.Method.POST) 247 { 248 final UriBuilder locationBuilder = 249 UriBuilder.fromUri(requestContext.getUriInfo().getBaseUri()); 250 if (bulkException.getPath() != null) 251 { 252 locationBuilder.path(bulkException.getPath()); 253 } 254 location = locationBuilder.build().toString(); 255 } 256 257 // Include the current ETag for PreconditionFailedExceptions 258 String version = null; 259 if(bulkException.getCause() instanceof PreconditionFailedException) 260 { 261 version = ((PreconditionFailedException) 262 bulkException.getCause()).getVersion(); 263 } 264 265 BulkOperation response = BulkOperation.createResponse( 266 method, bulkException.getBulkId(), version, 267 location, status); 268 bulkStreamResponse.writeBulkOperation(response); 269 errorCount++; 270 return errorCount != failOnErrors; 271 } 272 else 273 { 274 return false; 275 } 276 } 277 278 279 280 /** 281 * Process an operation from a bulk request. 282 * 283 * @param operation The operation to be processed from the bulk request. 284 * 285 * @return The operation response. 286 * @throws BulkException If an error occurs while processing the individual 287 * operation within the bulk operation. 288 */ 289 private BulkOperation processOperation(final BulkOperation operation) 290 throws BulkException 291 { 292 final Method method = operation.getMethod(); 293 final String bulkId = operation.getBulkId(); 294 final String path = operation.getPath(); 295 final String etag = operation.getVersion(); 296 final BaseResource resource = operation.getData(); 297 298 int statusCode = 200; 299 String location = null; 300 String endpoint = null; 301 String resourceID = null; 302 String responseVersion = null; 303 304 final ResourceDescriptor descriptor; 305 final ResourceStats resourceStats; 306 try 307 { 308 if (method == null) 309 { 310 throw new InvalidResourceException( 311 "The bulk operation does not specify a HTTP method"); 312 } 313 314 try 315 { 316 Method.valueOf(method.name()); 317 } 318 catch (IllegalArgumentException e) 319 { 320 throw new BulkException(SCIMException.createException( 321 405, "The bulk operation specifies an invalid " + 322 "HTTP method '" + method + "'. Allowed methods are " + 323 Arrays.asList(BulkOperation.Method.values())), 324 method, bulkId, path); 325 } 326 327 if (path == null) 328 { 329 throw new InvalidResourceException( 330 "The bulk operation does not specify a path"); 331 } 332 333 if (path != null) 334 { 335 // Parse the path into an endpoint and optional resource ID. 336 int startPos = 0; 337 if (path.charAt(startPos) == '/') 338 { 339 startPos++; 340 } 341 342 int endPos = path.indexOf('/', startPos); 343 if (endPos == -1) 344 { 345 endPos = path.length(); 346 } 347 348 endpoint = path.substring(startPos, endPos); 349 350 if (endPos < path.length() - 1) 351 { 352 resourceID = path.substring(endPos+1); 353 } 354 355 if (method == BulkOperation.Method.POST) 356 { 357 if (resourceID != null) 358 { 359 throw new InvalidResourceException( 360 "The bulk operation has method POST but the path includes" + 361 "a resource ID"); 362 } 363 } 364 else 365 { 366 if (resourceID == null) 367 { 368 throw new InvalidResourceException( 369 "The bulk operation does not have a resource ID in " + 370 "the path"); 371 } 372 373 if (resourceID.startsWith("bulkId:")) 374 { 375 final String ref = resourceID.substring(7); 376 resourceID = resourceIDs.get(ref); 377 if (resourceID == null) 378 { 379 throw SCIMException.createException( 380 409, "Cannot resolve bulkId reference '" + ref + "'"); 381 } 382 } 383 } 384 } 385 386 descriptor = getResourceDescriptor(endpoint); 387 if (descriptor == null) 388 { 389 throw new InvalidResourceException( 390 "The bulk operation specifies an unknown resource " + 391 "endpoint '" + endpoint + "'"); 392 } 393 394 resourceStats = application.getStatsForResource(descriptor.getName()); 395 if (resourceStats == null) 396 { 397 throw new ServerErrorException( 398 "Cannot find resource stats for resource '" + 399 descriptor.getName() + "'"); 400 } 401 } 402 catch (SCIMException e) 403 { 404 throw new BulkException(e, method, bulkId, path); 405 } 406 407 final UriBuilder locationBuilder = 408 UriBuilder.fromUri(requestContext.getUriInfo().getBaseUri()); 409 locationBuilder.path(path); 410 411 try 412 { 413 if (method == BulkOperation.Method.POST && bulkId == null) 414 { 415 throw new InvalidResourceException( 416 "The bulk operation has method POST but does not " + 417 "specify a bulkId"); 418 } 419 420 if (bulkId != null) 421 { 422 if (!bulkIds.add(bulkId)) 423 { 424 throw new InvalidResourceException( 425 "The bulk operation defines a duplicate bulkId '" + 426 bulkId + "'"); 427 } 428 } 429 430 if (method != BulkOperation.Method.DELETE && resource == null) 431 { 432 throw new InvalidResourceException( 433 "The bulk operation does not have any resource data"); 434 } 435 436 if (!unresolvedBulkIdRefs.isEmpty()) 437 { 438 throw SCIMException.createException( 439 409, "Cannot resolve bulkId references " 440 + unresolvedBulkIdRefs); 441 } 442 443 if (requestContext.getConsumeMediaType().equals( 444 MediaType.APPLICATION_JSON_TYPE)) 445 { 446 switch (method) 447 { 448 case POST: 449 resourceStats.incrementStat(ResourceStats.POST_CONTENT_JSON); 450 break; 451 case PUT: 452 resourceStats.incrementStat(ResourceStats.PUT_CONTENT_JSON); 453 break; 454 case PATCH: 455 resourceStats.incrementStat(ResourceStats.PATCH_CONTENT_JSON); 456 break; 457 } 458 } 459 else 460 { 461 switch (method) 462 { 463 case POST: 464 resourceStats.incrementStat(ResourceStats.POST_CONTENT_XML); 465 break; 466 case PUT: 467 resourceStats.incrementStat(ResourceStats.PUT_CONTENT_XML); 468 break; 469 case PATCH: 470 resourceStats.incrementStat(ResourceStats.PATCH_CONTENT_XML); 471 } 472 } 473 474 // Request no attributes because we will not provide the resource in 475 // the response. 476 final SCIMQueryAttributes queryAttributes = 477 new SCIMQueryAttributes(descriptor, ""); 478 479 switch (method) 480 { 481 case POST: 482 PostResourceRequest postResourceRequest = 483 new PostResourceRequest(requestContext.getUriInfo().getBaseUri(), 484 requestContext.getAuthID(), 485 descriptor, 486 resource.getScimObject(), 487 queryAttributes, 488 requestContext.getRequest()); 489 490 if (requestContext.getAuthID() == null) 491 { 492 AtomicReference<String> authIDRef = new AtomicReference<String>(); 493 Response response = validateOAuthToken(requestContext, 494 postResourceRequest, authIDRef, tokenHandler); 495 if (response != null) 496 { 497 throw new UnauthorizedException("Invalid credentials"); 498 } 499 else 500 { 501 String authID = authIDRef.get(); 502 postResourceRequest = new PostResourceRequest( 503 requestContext.getUriInfo().getBaseUri(), 504 authID, descriptor, resource.getScimObject(), 505 queryAttributes, 506 requestContext.getRequest()); 507 } 508 } 509 510 final BaseResource postedResource = 511 backend.postResource(postResourceRequest); 512 513 resourceID = postedResource.getId(); 514 responseVersion = postedResource.getMeta().getVersion(); 515 locationBuilder.path(resourceID); 516 statusCode = 201; 517 resourceStats.incrementStat(ResourceStats.POST_OK); 518 break; 519 520 case PUT: 521 PutResourceRequest putResourceRequest = 522 new PutResourceRequest(requestContext.getUriInfo().getBaseUri(), 523 requestContext.getAuthID(), 524 descriptor, 525 resourceID, 526 resource.getScimObject(), 527 queryAttributes, 528 requestContext.getRequest(), 529 etag, null); 530 531 if (requestContext.getAuthID() == null) 532 { 533 AtomicReference<String> authIDRef = new AtomicReference<String>(); 534 Response response = validateOAuthToken(requestContext, 535 putResourceRequest, authIDRef, tokenHandler); 536 if (response != null) 537 { 538 throw new UnauthorizedException("Invalid credentials"); 539 } 540 else 541 { 542 String authID = authIDRef.get(); 543 putResourceRequest = new PutResourceRequest( 544 requestContext.getUriInfo().getBaseUri(), 545 authID, descriptor, resourceID, resource.getScimObject(), 546 queryAttributes, requestContext.getRequest(), 547 etag, null); 548 } 549 } 550 551 responseVersion = 552 backend.putResource(putResourceRequest).getMeta().getVersion(); 553 resourceStats.incrementStat(ResourceStats.PUT_OK); 554 break; 555 556 case PATCH: 557 PatchResourceRequest patchResourceRequest = 558 new PatchResourceRequest(requestContext.getUriInfo().getBaseUri(), 559 requestContext.getAuthID(), 560 descriptor, 561 resourceID, 562 resource.getScimObject(), 563 queryAttributes, 564 requestContext.getRequest(), 565 etag, null); 566 567 if (requestContext.getAuthID() == null) 568 { 569 AtomicReference<String> authIDRef = new AtomicReference<String>(); 570 Response response = validateOAuthToken(requestContext, 571 patchResourceRequest, authIDRef, tokenHandler); 572 if (response != null) 573 { 574 throw new UnauthorizedException("Invalid credentials"); 575 } 576 else 577 { 578 String authID = authIDRef.get(); 579 patchResourceRequest = new PatchResourceRequest( 580 requestContext.getUriInfo().getBaseUri(), 581 authID, descriptor, resourceID, resource.getScimObject(), 582 queryAttributes, requestContext.getRequest(), 583 etag, null); 584 } 585 } 586 587 responseVersion = 588 backend.patchResource(patchResourceRequest).getMeta().getVersion(); 589 resourceStats.incrementStat(ResourceStats.PATCH_OK); 590 break; 591 592 case DELETE: 593 DeleteResourceRequest deleteResourceRequest = 594 new DeleteResourceRequest(requestContext.getUriInfo().getBaseUri(), 595 requestContext.getAuthID(), 596 descriptor, 597 resourceID, 598 requestContext.getRequest(), 599 etag, null); 600 601 if (requestContext.getAuthID() == null) 602 { 603 AtomicReference<String> authIDRef = new AtomicReference<String>(); 604 Response response = validateOAuthToken(requestContext, 605 deleteResourceRequest, authIDRef, tokenHandler); 606 if (response != null) 607 { 608 throw new UnauthorizedException("Invalid credentials"); 609 } 610 else 611 { 612 String authID = authIDRef.get(); 613 deleteResourceRequest = new DeleteResourceRequest( 614 requestContext.getUriInfo().getBaseUri(), 615 authID, descriptor, resourceID, 616 requestContext.getRequest(), etag, null); 617 } 618 } 619 620 backend.deleteResource(deleteResourceRequest); 621 resourceStats.incrementStat(ResourceStats.DELETE_OK); 622 break; 623 } 624 625 if (bulkId != null) 626 { 627 resourceIDs.put(bulkId, resourceID); 628 } 629 } 630 catch (SCIMException e) 631 { 632 switch (method) 633 { 634 case POST: 635 resourceStats.incrementStat("post-" + e.getStatusCode()); 636 break; 637 case PUT: 638 resourceStats.incrementStat("put-" + e.getStatusCode()); 639 break; 640 case PATCH: 641 resourceStats.incrementStat("patch-" + e.getStatusCode()); 642 break; 643 case DELETE: 644 resourceStats.incrementStat("delete-" + e.getStatusCode()); 645 break; 646 } 647 throw new BulkException(e, method, bulkId, path); 648 } 649 650 if (requestContext.getProduceMediaType() == 651 MediaType.APPLICATION_JSON_TYPE) 652 { 653 switch (method) 654 { 655 case POST: 656 resourceStats.incrementStat(ResourceStats.POST_RESPONSE_JSON); 657 break; 658 case PUT: 659 resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_JSON); 660 break; 661 case PATCH: 662 resourceStats.incrementStat(ResourceStats.PATCH_RESPONSE_JSON); 663 break; 664 } 665 } 666 else if (requestContext.getProduceMediaType() == 667 MediaType.APPLICATION_XML_TYPE) 668 { 669 switch (method) 670 { 671 case POST: 672 resourceStats.incrementStat(ResourceStats.POST_RESPONSE_XML); 673 break; 674 case PUT: 675 resourceStats.incrementStat(ResourceStats.PUT_RESPONSE_XML); 676 break; 677 case PATCH: 678 resourceStats.incrementStat(ResourceStats.PATCH_RESPONSE_XML); 679 } 680 } 681 682 // Set the location for all operations except an unsuccessful POST. 683 if (method != BulkOperation.Method.POST || statusCode == 201) 684 { 685 location = locationBuilder.build().toString(); 686 } 687 688 final Status status = 689 new Status(String.valueOf(statusCode), null); 690 691 return BulkOperation.createResponse(method, bulkId, responseVersion, 692 location, status); 693 } 694 695 696 697 /** 698 * Obtain a copy of the provided resource with each bulkId reference 699 * resolved to a resource ID. 700 * 701 * @param resource The resource that may contain bulkId references. 702 * 703 * @return A copy of the resource with bulkId references resolved. 704 * 705 * @throws SCIMException If there are any undefined bulkId references. 706 */ 707 private BaseResource resolveBulkIds(final BaseResource resource) 708 throws SCIMException 709 { 710 final SCIMObject src = resource.getScimObject(); 711 final SCIMObject dst = new SCIMObject(); 712 713 for (final String schema : src.getSchemas()) 714 { 715 for (final SCIMAttribute a : src.getAttributes(schema)) 716 { 717 dst.setAttribute(resolveBulkIds(a)); 718 } 719 } 720 721 return new BaseResource(resource.getResourceDescriptor(), dst); 722 } 723 724 725 726 /** 727 * Obtain a copy of the provided SCIM attribute with each bulkId reference 728 * resolved to a resource ID. 729 * 730 * @param a The attribute that may contain bulkId references. 731 * 732 * @return A copy of the attribute with bulkId references resolved. 733 * 734 * @throws SCIMException If there are any undefined bulkId references. 735 */ 736 private SCIMAttribute resolveBulkIds(final SCIMAttribute a) 737 throws SCIMException 738 { 739 final SCIMAttributeValue[] srcValues = a.getValues(); 740 final SCIMAttributeValue[] dstValues = 741 new SCIMAttributeValue[a.getValues().length]; 742 for (int i = 0; i < srcValues.length; i++) 743 { 744 dstValues[i] = resolveBulkIds(srcValues[i]); 745 } 746 747 return SCIMAttribute.create(a.getAttributeDescriptor(), dstValues); 748 } 749 750 751 752 /** 753 * Obtain a copy of the provided SCIM attribute value with each bulkId 754 * reference resolved to a resource ID. 755 * 756 * @param v The attribute value that may contain bulkId references. 757 * 758 * @return A copy of the attribute value with bulkId references resolved. 759 * 760 * @throws SCIMException If there are any undefined bulkId references. 761 */ 762 private SCIMAttributeValue resolveBulkIds(final SCIMAttributeValue v) 763 throws SCIMException 764 { 765 if (v.isComplex()) 766 { 767 final Collection<SCIMAttribute> srcAttrs = v.getAttributes().values(); 768 final ArrayList<SCIMAttribute> dstAttrs = 769 new ArrayList<SCIMAttribute>(srcAttrs.size()); 770 for (final SCIMAttribute a : srcAttrs) 771 { 772 dstAttrs.add(resolveBulkIds(a)); 773 } 774 775 return SCIMAttributeValue.createComplexValue(dstAttrs); 776 } 777 else 778 { 779 final String s = v.getStringValue(); 780 if (s.startsWith("bulkId:")) 781 { 782 final String bulkId = s.substring(7); 783 final String resourceID = resourceIDs.get(bulkId); 784 if (resourceID != null) 785 { 786 return SCIMAttributeValue.createStringValue(resourceID); 787 } 788 else 789 { 790 throw SCIMException.createException( 791 409, "Cannot resolve bulkId reference '" + bulkId + "'"); 792 } 793 } 794 else 795 { 796 return v; 797 } 798 } 799 } 800}