001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2012-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.json.resource; 018 019import static org.forgerock.json.resource.Responses.newQueryResponse; 020import static org.forgerock.json.resource.Responses.newResourceResponse; 021import static org.forgerock.util.promise.Promises.newExceptionPromise; 022import static org.forgerock.util.promise.Promises.newResultPromise; 023 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.Comparator; 027import java.util.Iterator; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.atomic.AtomicLong; 034 035import org.forgerock.services.context.Context; 036import org.forgerock.json.JsonPointer; 037import org.forgerock.json.JsonValue; 038import org.forgerock.json.JsonValueException; 039import org.forgerock.util.encode.Base64; 040import org.forgerock.util.promise.Promise; 041import org.forgerock.util.query.QueryFilter; 042import org.forgerock.util.query.QueryFilterVisitor; 043 044/** 045 * A simple in-memory collection resource provider which uses a {@code Map} to 046 * store resources. This resource provider is intended for testing purposes only 047 * and there are no performance guarantees. 048 */ 049public final class MemoryBackend implements CollectionResourceProvider { 050 private enum FilterResult { 051 FALSE, TRUE, UNDEFINED; 052 053 static FilterResult valueOf(final boolean b) { 054 return b ? TRUE : FALSE; 055 } 056 057 boolean toBoolean() { 058 return this == TRUE; // UNDEFINED collapses to FALSE. 059 } 060 } 061 062 private static final class Cookie { 063 private final List<SortKey> sortKeys; 064 private final int lastResultIndex; 065 066 Cookie(final int lastResultIndex, final List<SortKey> sortKeys) { 067 this.sortKeys = sortKeys; 068 this.lastResultIndex = lastResultIndex; 069 } 070 071 static Cookie valueOf(String base64) { 072 final String decoded = new String(Base64.decode(base64)); 073 final String[] split = decoded.split(":"); 074 final int lastOffset = Integer.parseInt(split[0]); 075 final List<SortKey> sortKeys = new ArrayList<>(); 076 final String[] splitKeys = split[1].split(","); 077 078 for (String key : splitKeys) { 079 if (!key.equals("")) { 080 sortKeys.add(SortKey.valueOf(key)); 081 } 082 } 083 084 return new Cookie(lastOffset, sortKeys); 085 } 086 087 String toBase64() { 088 final StringBuilder buf = new StringBuilder(); 089 buf.append(lastResultIndex).append(":"); 090 091 for (int i = 0; i < sortKeys.size(); i++) { 092 if (i > 0) { 093 buf.append(","); 094 } 095 buf.append(sortKeys.get(i).toString()); 096 } 097 098 return Base64.encode(buf.toString().getBytes()); 099 } 100 101 public List<SortKey> getSortKeys() { 102 return sortKeys; 103 } 104 105 public int getLastResultIndex() { 106 return lastResultIndex; 107 } 108 } 109 110 private static final class ResourceComparator implements Comparator<ResourceResponse> { 111 private final List<SortKey> sortKeys; 112 113 private ResourceComparator(final List<SortKey> sortKeys) { 114 this.sortKeys = sortKeys; 115 } 116 117 @Override 118 public int compare(final ResourceResponse r1, final ResourceResponse r2) { 119 for (final SortKey sortKey : sortKeys) { 120 final int result = compare(r1, r2, sortKey); 121 if (result != 0) { 122 return result; 123 } 124 } 125 return 0; 126 } 127 128 private int compare(final ResourceResponse r1, final ResourceResponse r2, final SortKey sortKey) { 129 final List<Object> vs1 = getValuesSorted(r1, sortKey.getField()); 130 final List<Object> vs2 = getValuesSorted(r2, sortKey.getField()); 131 if (vs1.isEmpty() && vs2.isEmpty()) { 132 return 0; 133 } else if (vs1.isEmpty()) { 134 // Sort resources with missing attributes last. 135 return 1; 136 } else if (vs2.isEmpty()) { 137 // Sort resources with missing attributes last. 138 return -1; 139 } else { 140 // Compare first values only (consistent with LDAP sort control). 141 final Object v1 = vs1.get(0); 142 final Object v2 = vs2.get(0); 143 return sortKey.isAscendingOrder() ? compareValues(v1, v2) : -compareValues(v1, v2); 144 } 145 } 146 147 private List<Object> getValuesSorted(final ResourceResponse resource, final JsonPointer field) { 148 final JsonValue value = resource.getContent().get(field); 149 if (value == null) { 150 return Collections.emptyList(); 151 } else if (value.isList()) { 152 List<Object> results = value.asList(); 153 if (results.size() > 1) { 154 results = new ArrayList<>(results); 155 Collections.sort(results, VALUE_COMPARATOR); 156 } 157 return results; 158 } else { 159 return Collections.singletonList(value.getObject()); 160 } 161 } 162 } 163 164 private static final QueryFilterVisitor<FilterResult, ResourceResponse, JsonPointer> RESOURCE_FILTER = 165 new QueryFilterVisitor<FilterResult, ResourceResponse, JsonPointer>() { 166 167 @Override 168 public FilterResult visitAndFilter(final ResourceResponse p, 169 final List<org.forgerock.util.query.QueryFilter<JsonPointer>> subFilters) { 170 FilterResult result = FilterResult.TRUE; 171 for (final org.forgerock.util.query.QueryFilter<JsonPointer> subFilter : subFilters) { 172 final FilterResult r = subFilter.accept(this, p); 173 if (r.ordinal() < result.ordinal()) { 174 result = r; 175 } 176 if (result == FilterResult.FALSE) { 177 break; 178 } 179 } 180 return result; 181 } 182 183 @Override 184 public FilterResult visitBooleanLiteralFilter(final ResourceResponse p, final boolean value) { 185 return FilterResult.valueOf(value); 186 } 187 188 @Override 189 public FilterResult visitContainsFilter(final ResourceResponse p, final JsonPointer field, 190 final Object valueAssertion) { 191 for (final Object value : getValues(p, field)) { 192 if (isCompatible(valueAssertion, value)) { 193 if (valueAssertion instanceof String) { 194 final String s1 = 195 ((String) valueAssertion).toLowerCase(Locale.ENGLISH); 196 final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); 197 if (s2.contains(s1)) { 198 return FilterResult.TRUE; 199 } 200 } else { 201 // Use equality matching for numbers and booleans. 202 if (compareValues(valueAssertion, value) == 0) { 203 return FilterResult.TRUE; 204 } 205 } 206 } 207 } 208 return FilterResult.FALSE; 209 } 210 211 @Override 212 public FilterResult visitEqualsFilter(final ResourceResponse p, final JsonPointer field, 213 final Object valueAssertion) { 214 for (final Object value : getValues(p, field)) { 215 if (isCompatible(valueAssertion, value) 216 && compareValues(valueAssertion, value) == 0) { 217 return FilterResult.TRUE; 218 } 219 } 220 return FilterResult.FALSE; 221 } 222 223 @Override 224 public FilterResult visitExtendedMatchFilter(final ResourceResponse p, 225 final JsonPointer field, final String matchingRuleId, 226 final Object valueAssertion) { 227 // This backend does not support any extended filters. 228 return FilterResult.UNDEFINED; 229 } 230 231 @Override 232 public FilterResult visitGreaterThanFilter(final ResourceResponse p, 233 final JsonPointer field, final Object valueAssertion) { 234 for (final Object value : getValues(p, field)) { 235 if (isCompatible(valueAssertion, value) 236 && compareValues(valueAssertion, value) < 0) { 237 return FilterResult.TRUE; 238 } 239 } 240 return FilterResult.FALSE; 241 } 242 243 @Override 244 public FilterResult visitGreaterThanOrEqualToFilter(final ResourceResponse p, 245 final JsonPointer field, final Object valueAssertion) { 246 for (final Object value : getValues(p, field)) { 247 if (isCompatible(valueAssertion, value) 248 && compareValues(valueAssertion, value) <= 0) { 249 return FilterResult.TRUE; 250 } 251 } 252 return FilterResult.FALSE; 253 } 254 255 @Override 256 public FilterResult visitLessThanFilter(final ResourceResponse p, final JsonPointer field, 257 final Object valueAssertion) { 258 for (final Object value : getValues(p, field)) { 259 if (isCompatible(valueAssertion, value) 260 && compareValues(valueAssertion, value) > 0) { 261 return FilterResult.TRUE; 262 } 263 } 264 return FilterResult.FALSE; 265 } 266 267 @Override 268 public FilterResult visitLessThanOrEqualToFilter(final ResourceResponse p, 269 final JsonPointer field, final Object valueAssertion) { 270 for (final Object value : getValues(p, field)) { 271 if (isCompatible(valueAssertion, value) 272 && compareValues(valueAssertion, value) >= 0) { 273 return FilterResult.TRUE; 274 } 275 } 276 return FilterResult.FALSE; 277 } 278 279 @Override 280 public FilterResult visitNotFilter(final ResourceResponse p, 281 final org.forgerock.util.query.QueryFilter<JsonPointer> subFilter) { 282 switch (subFilter.accept(this, p)) { 283 case FALSE: 284 return FilterResult.TRUE; 285 case UNDEFINED: 286 return FilterResult.UNDEFINED; 287 default: // TRUE 288 return FilterResult.FALSE; 289 } 290 } 291 292 @Override 293 public FilterResult visitOrFilter(final ResourceResponse p, 294 final List<org.forgerock.util.query.QueryFilter<JsonPointer>> subFilters) { 295 FilterResult result = FilterResult.FALSE; 296 for (final org.forgerock.util.query.QueryFilter<JsonPointer> subFilter : subFilters) { 297 final FilterResult r = subFilter.accept(this, p); 298 if (r.ordinal() > result.ordinal()) { 299 result = r; 300 } 301 if (result == FilterResult.TRUE) { 302 break; 303 } 304 } 305 return result; 306 } 307 308 @Override 309 public FilterResult visitPresentFilter(final ResourceResponse p, final JsonPointer field) { 310 final JsonValue value = p.getContent().get(field); 311 return FilterResult.valueOf(value != null); 312 } 313 314 @Override 315 public FilterResult visitStartsWithFilter(final ResourceResponse p, 316 final JsonPointer field, final Object valueAssertion) { 317 for (final Object value : getValues(p, field)) { 318 if (isCompatible(valueAssertion, value)) { 319 if (valueAssertion instanceof String) { 320 final String s1 = 321 ((String) valueAssertion).toLowerCase(Locale.ENGLISH); 322 final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); 323 if (s2.startsWith(s1)) { 324 return FilterResult.TRUE; 325 } 326 } else { 327 // Use equality matching for numbers and booleans. 328 if (compareValues(valueAssertion, value) == 0) { 329 return FilterResult.TRUE; 330 } 331 } 332 } 333 } 334 return FilterResult.FALSE; 335 } 336 337 private List<Object> getValues(final ResourceResponse resource, final JsonPointer field) { 338 final JsonValue value = resource.getContent().get(field); 339 if (value == null) { 340 return Collections.emptyList(); 341 } else if (value.isList()) { 342 return value.asList(); 343 } else { 344 return Collections.singletonList(value.getObject()); 345 } 346 } 347 348 }; 349 350 private static final Comparator<Object> VALUE_COMPARATOR = new Comparator<Object>() { 351 @Override 352 public int compare(final Object o1, final Object o2) { 353 return compareValues(o1, o2); 354 } 355 }; 356 357 private static int compareValues(final Object v1, final Object v2) { 358 if (v1 instanceof String && v2 instanceof String) { 359 final String s1 = (String) v1; 360 final String s2 = (String) v2; 361 return s1.compareToIgnoreCase(s2); 362 } else if (v1 instanceof Number && v2 instanceof Number) { 363 final Double n1 = ((Number) v1).doubleValue(); 364 final Double n2 = ((Number) v2).doubleValue(); 365 return n1.compareTo(n2); 366 } else if (v1 instanceof Boolean && v2 instanceof Boolean) { 367 final Boolean b1 = (Boolean) v1; 368 final Boolean b2 = (Boolean) v2; 369 return b1.compareTo(b2); 370 } else { 371 // Different types: we need to ensure predictable ordering, 372 // so use class name as secondary key. 373 return v1.getClass().getName().compareTo(v2.getClass().getName()); 374 } 375 } 376 377 private static boolean isCompatible(final Object v1, final Object v2) { 378 return (v1 instanceof String && v2 instanceof String) 379 || (v1 instanceof Number && v2 instanceof Number) 380 || (v1 instanceof Boolean && v2 instanceof Boolean); 381 } 382 383 private final AtomicLong nextResourceId = new AtomicLong(); 384 private final Map<String, ResourceResponse> resources = new ConcurrentHashMap<>(); 385 private final Object writeLock = new Object(); 386 387 /** 388 * Creates a new in-memory collection containing no resources. 389 */ 390 public MemoryBackend() { 391 // No implementation required. 392 } 393 394 /** 395 * {@inheritDoc} 396 */ 397 @Override 398 public Promise<ActionResponse, ResourceException> actionCollection(final Context context, 399 final ActionRequest request) { 400 try { 401 if (request.getAction().equals("clear")) { 402 final int size; 403 synchronized (writeLock) { 404 size = resources.size(); 405 resources.clear(); 406 } 407 final JsonValue result = new JsonValue(new LinkedHashMap<>(1)); 408 result.put("cleared", size); 409 return newResultPromise(Responses.newActionResponse(result)); 410 } else { 411 throw new NotSupportedException("Unrecognized action ID '" + request.getAction() 412 + "'. Supported action IDs: clear"); 413 } 414 } catch (final ResourceException e) { 415 return newExceptionPromise(e); 416 } 417 } 418 419 /** 420 * {@inheritDoc} 421 */ 422 @Override 423 public Promise<ActionResponse, ResourceException> actionInstance(final Context context, final String id, 424 final ActionRequest request) { 425 final ResourceException e = 426 new NotSupportedException("Actions are not supported for resource instances"); 427 return newExceptionPromise(e); 428 } 429 430 /** 431 * {@inheritDoc} 432 */ 433 @Override 434 public Promise<ResourceResponse, ResourceException> createInstance(final Context context, 435 final CreateRequest request) { 436 final JsonValue value = request.getContent(); 437 final String id = request.getNewResourceId(); 438 final String rev = "0"; 439 try { 440 final ResourceResponse resource; 441 while (true) { 442 final String eid = 443 id != null ? id : String.valueOf(nextResourceId.getAndIncrement()); 444 final ResourceResponse tmp = newResourceResponse(eid, rev, value); 445 synchronized (writeLock) { 446 final ResourceResponse existingResource = resources.put(eid, tmp); 447 if (existingResource != null) { 448 if (id != null) { 449 // Already exists - put the existing resource back. 450 resources.put(id, existingResource); 451 throw new PreconditionFailedException("The resource with ID '" + id 452 + "' could not be created because " 453 + "there is already another resource with the same ID"); 454 } else { 455 // Retry with next available resource ID. 456 } 457 } else { 458 // Add succeeded. 459 addIdAndRevision(tmp); 460 resource = tmp; 461 break; 462 } 463 } 464 } 465 return newResultPromise(resource); 466 } catch (final ResourceException e) { 467 return newExceptionPromise(e); 468 } 469 } 470 471 /** 472 * {@inheritDoc} 473 */ 474 @Override 475 public Promise<ResourceResponse, ResourceException> deleteInstance(final Context context, final String id, 476 final DeleteRequest request) { 477 final String rev = request.getRevision(); 478 try { 479 final ResourceResponse resource; 480 synchronized (writeLock) { 481 resource = getResourceForUpdate(id, rev); 482 resources.remove(id); 483 } 484 return newResultPromise(resource); 485 } catch (final ResourceException e) { 486 return newExceptionPromise(e); 487 } 488 } 489 490 /** 491 * {@inheritDoc} 492 */ 493 @Override 494 public Promise<ResourceResponse, ResourceException> patchInstance(final Context context, final String id, 495 final PatchRequest request) { 496 final String rev = request.getRevision(); 497 try { 498 final ResourceResponse resource; 499 synchronized (writeLock) { 500 final ResourceResponse existingResource = getResourceForUpdate(id, rev); 501 final String newRev = getNextRevision(existingResource.getRevision()); 502 final JsonValue newContent = existingResource.getContent().copy(); 503 for (final PatchOperation operation : request.getPatchOperations()) { 504 try { 505 if (operation.isAdd()) { 506 newContent.putPermissive(operation.getField(), operation.getValue() 507 .getObject()); 508 } else if (operation.isRemove()) { 509 if (operation.getValue().isNull()) { 510 // Remove entire value. 511 newContent.remove(operation.getField()); 512 } else { 513 // Find matching value(s) and remove (assumes reference to array). 514 final JsonValue value = newContent.get(operation.getField()); 515 if (value != null) { 516 if (value.isList()) { 517 final Object valueToBeRemoved = 518 operation.getValue().getObject(); 519 final Iterator<Object> iterator = value.asList().iterator(); 520 while (iterator.hasNext()) { 521 if (valueToBeRemoved.equals(iterator.next())) { 522 iterator.remove(); 523 } 524 } 525 } else { 526 // Single valued field. 527 final Object valueToBeRemoved = 528 operation.getValue().getObject(); 529 if (valueToBeRemoved.equals(value.getObject())) { 530 newContent.remove(operation.getField()); 531 } 532 } 533 } 534 } 535 } else if (operation.isReplace()) { 536 newContent.remove(operation.getField()); 537 if (!operation.getValue().isNull()) { 538 newContent.putPermissive(operation.getField(), operation.getValue() 539 .getObject()); 540 } 541 } else if (operation.isIncrement()) { 542 final JsonValue value = newContent.get(operation.getField()); 543 final Number amount = operation.getValue().asNumber(); 544 if (value == null) { 545 throw new BadRequestException("The field '" + operation.getField() 546 + "' does not exist"); 547 } else if (value.isList()) { 548 final List<Object> elements = value.asList(); 549 for (int i = 0; i < elements.size(); i++) { 550 elements.set(i, increment(operation, elements.get(i), amount)); 551 } 552 } else { 553 newContent.put(operation.getField(), increment(operation, value 554 .getObject(), amount)); 555 } 556 } 557 } catch (final JsonValueException e) { 558 throw new ConflictException("The field '" + operation.getField() 559 + "' does not exist"); 560 } 561 } 562 resource = newResourceResponse(id, newRev, newContent); 563 addIdAndRevision(resource); 564 resources.put(id, resource); 565 } 566 return newResultPromise(resource); 567 } catch (final ResourceException e) { 568 return newExceptionPromise(e); 569 } 570 } 571 572 /** 573 * {@inheritDoc} 574 */ 575 @Override 576 public Promise<QueryResponse, ResourceException> queryCollection(final Context context, 577 final QueryRequest request, final QueryResourceHandler handler) { 578 if (request.getQueryId() != null) { 579 return new NotSupportedException("Query by ID not supported").asPromise(); 580 } else if (request.getQueryExpression() != null) { 581 return new NotSupportedException("Query by expression not supported").asPromise(); 582 } else { 583 // No filtering or query by filter. 584 final QueryFilter<JsonPointer> filter = request.getQueryFilter(); 585 586 // If paged results are requested then decode the cookie in order to determine 587 // the index of the first result to be returned. 588 final int pageSize = request.getPageSize(); 589 final String pagedResultsCookie = request.getPagedResultsCookie(); 590 final boolean pagedResultsRequested = pageSize > 0; 591 final int firstResultIndex; 592 final List<SortKey> sortKeys = request.getSortKeys(); 593 594 if (pageSize > 0 && pagedResultsCookie != null) { 595 if (request.getPagedResultsOffset() > 0) { 596 return new BadRequestException("Cookies and offsets are mutually exclusive").asPromise(); 597 } 598 599 firstResultIndex = Cookie.valueOf(pagedResultsCookie).getLastResultIndex(); 600 } else { 601 if (request.getPagedResultsOffset() > 0) { 602 firstResultIndex = request.getPagedResultsOffset(); 603 } else { 604 firstResultIndex = 0; 605 } 606 } 607 608 final int lastResultIndex = 609 pagedResultsRequested ? firstResultIndex + pageSize : Integer.MAX_VALUE; 610 611 // Select, filter, and return the results. These can be streamed if server 612 // side sorting has not been requested. 613 int resultIndex = 0; 614 int resultCount; 615 if (sortKeys.isEmpty()) { 616 // No sorting so stream the results. 617 for (final ResourceResponse resource : resources.values()) { 618 if (filter == null || filter.accept(RESOURCE_FILTER, resource).toBoolean()) { 619 if (resultIndex >= firstResultIndex && resultIndex < lastResultIndex) { 620 handler.handleResource(resource); 621 } 622 resultIndex++; 623 } 624 } 625 626 resultCount = resources.values().size(); 627 } else { 628 // Server side sorting: aggregate the result set then sort. A robust implementation 629 // would need to impose administrative limits in order to control memory utilization. 630 final List<ResourceResponse> results = new ArrayList<>(); 631 for (final ResourceResponse resource : resources.values()) { 632 if (filter == null || filter.accept(RESOURCE_FILTER, resource).toBoolean()) { 633 results.add(resource); 634 } 635 } 636 Collections.sort(results, new ResourceComparator(sortKeys)); 637 for (final ResourceResponse resource : results) { 638 if (resultIndex >= firstResultIndex && resultIndex < lastResultIndex) { 639 handler.handleResource(resource); 640 } 641 642 if (resultIndex < lastResultIndex) { 643 resultIndex++; 644 } else { 645 break; 646 } 647 } 648 649 resultCount = results.size(); 650 } 651 652 if (pagedResultsRequested) { 653 final String nextCookie = resultIndex < resources.size() 654 ? new Cookie(lastResultIndex, sortKeys).toBase64() 655 : null; 656 657 switch (request.getTotalPagedResultsPolicy()) { 658 case NONE: 659 return newResultPromise(newQueryResponse(nextCookie)); 660 case EXACT: 661 case ESTIMATE: 662 return newResultPromise(newQueryResponse(nextCookie, CountPolicy.EXACT, resultCount)); 663 default: 664 throw new UnsupportedOperationException("totalPagedResultsPolicy: " 665 + request.getTotalPagedResultsPolicy().toString() + " not supported"); 666 } 667 } else { 668 return newResultPromise(newQueryResponse()); 669 } 670 } 671 } 672 673 /** 674 * {@inheritDoc} 675 */ 676 @Override 677 public Promise<ResourceResponse, ResourceException> readInstance(final Context context, final String id, 678 final ReadRequest request) { 679 try { 680 final ResourceResponse resource = resources.get(id); 681 if (resource == null) { 682 throw new NotFoundException("The resource with ID '" + id 683 + "' could not be read because it does not exist"); 684 } 685 return newResultPromise(resource); 686 } catch (final ResourceException e) { 687 return newExceptionPromise(e); 688 } 689 } 690 691 /** 692 * {@inheritDoc} 693 */ 694 @Override 695 public Promise<ResourceResponse, ResourceException> updateInstance(final Context context, final String id, 696 final UpdateRequest request) { 697 final String rev = request.getRevision(); 698 try { 699 final ResourceResponse resource; 700 synchronized (writeLock) { 701 final ResourceResponse existingResource = getResourceForUpdate(id, rev); 702 final String newRev = getNextRevision(existingResource.getRevision()); 703 resource = newResourceResponse(id, newRev, request.getContent()); 704 addIdAndRevision(resource); 705 resources.put(id, resource); 706 } 707 return newResultPromise(resource); 708 } catch (final ResourceException e) { 709 return newExceptionPromise(e); 710 } 711 } 712 713 /* 714 * Add the ID and revision to the JSON content so that they are included 715 * with subsequent responses. We shouldn't really update the passed in 716 * content in case it is shared by other components, but we'll do it here 717 * anyway for simplicity. 718 */ 719 private void addIdAndRevision(final ResourceResponse resource) throws ResourceException { 720 final JsonValue content = resource.getContent(); 721 try { 722 content.asMap().put(ResourceResponse.FIELD_CONTENT_ID, resource.getId()); 723 content.asMap().put(ResourceResponse.FIELD_CONTENT_REVISION, resource.getRevision()); 724 } catch (final JsonValueException e) { 725 throw new BadRequestException( 726 "The request could not be processed because the provided " 727 + "content is not a JSON object"); 728 } 729 } 730 731 private String getNextRevision(final String rev) throws ResourceException { 732 try { 733 return String.valueOf(Integer.parseInt(rev) + 1); 734 } catch (final NumberFormatException e) { 735 throw new InternalServerErrorException("Malformed revision number '" + rev 736 + "' encountered while updating a resource"); 737 } 738 } 739 740 private ResourceResponse getResourceForUpdate(final String id, final String rev) 741 throws NotFoundException, PreconditionFailedException { 742 final ResourceResponse existingResource = resources.get(id); 743 if (existingResource == null) { 744 throw new NotFoundException("The resource with ID '" + id 745 + "' could not be updated because it does not exist"); 746 } else if (rev != null && !existingResource.getRevision().equals(rev)) { 747 throw new PreconditionFailedException("The resource with ID '" + id 748 + "' could not be updated because " + "it does not have the required version"); 749 } 750 return existingResource; 751 } 752 753 private Object increment(final PatchOperation operation, final Object object, 754 final Number amount) throws BadRequestException { 755 if (object instanceof Long) { 756 return ((Long) object) + amount.longValue(); 757 } else if (object instanceof Integer) { 758 return ((Integer) object) + amount.intValue(); 759 } else if (object instanceof Float) { 760 return ((Float) object) + amount.floatValue(); 761 } else if (object instanceof Double) { 762 return ((Double) object) + amount.doubleValue(); 763 } else { 764 throw new BadRequestException("The field '" + operation.getField() 765 + "' is not a number"); 766 } 767 } 768}