View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2012-2015 ForgeRock AS.
15   */
16  
17  package org.forgerock.json.resource;
18  
19  import static org.forgerock.json.resource.Responses.newQueryResponse;
20  import static org.forgerock.json.resource.Responses.newResourceResponse;
21  import static org.forgerock.util.promise.Promises.newExceptionPromise;
22  import static org.forgerock.util.promise.Promises.newResultPromise;
23  
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Comparator;
27  import java.util.Iterator;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.concurrent.atomic.AtomicLong;
34  
35  import org.forgerock.services.context.Context;
36  import org.forgerock.json.JsonPointer;
37  import org.forgerock.json.JsonValue;
38  import org.forgerock.json.JsonValueException;
39  import org.forgerock.util.encode.Base64;
40  import org.forgerock.util.promise.Promise;
41  import org.forgerock.util.query.QueryFilter;
42  import org.forgerock.util.query.QueryFilterVisitor;
43  
44  /**
45   * A simple in-memory collection resource provider which uses a {@code Map} to
46   * store resources. This resource provider is intended for testing purposes only
47   * and there are no performance guarantees.
48   */
49  public final class MemoryBackend implements CollectionResourceProvider {
50      private enum FilterResult {
51          FALSE, TRUE, UNDEFINED;
52  
53          static FilterResult valueOf(final boolean b) {
54              return b ? TRUE : FALSE;
55          }
56  
57          boolean toBoolean() {
58              return this == TRUE; // UNDEFINED collapses to FALSE.
59          }
60      }
61  
62      private static final class Cookie {
63          private final List<SortKey> sortKeys;
64          private final int lastResultIndex;
65  
66          Cookie(final int lastResultIndex, final List<SortKey> sortKeys) {
67              this.sortKeys = sortKeys;
68              this.lastResultIndex = lastResultIndex;
69          }
70  
71          static Cookie valueOf(String base64) {
72              final String decoded = new String(Base64.decode(base64));
73              final String[] split = decoded.split(":");
74              final int lastOffset = Integer.parseInt(split[0]);
75              final List<SortKey> sortKeys = new ArrayList<>();
76              final String[] splitKeys = split[1].split(",");
77  
78              for (String key : splitKeys) {
79                  if (!key.equals("")) {
80                      sortKeys.add(SortKey.valueOf(key));
81                  }
82              }
83  
84              return new Cookie(lastOffset, sortKeys);
85          }
86  
87          String toBase64() {
88              final StringBuilder buf = new StringBuilder();
89              buf.append(lastResultIndex).append(":");
90  
91              for (int i = 0; i < sortKeys.size(); i++) {
92                  if (i > 0) {
93                      buf.append(",");
94                  }
95                  buf.append(sortKeys.get(i).toString());
96              }
97  
98              return Base64.encode(buf.toString().getBytes());
99          }
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 }