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}