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