1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
46
47
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;
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
135 return 1;
136 } else if (vs2.isEmpty()) {
137
138 return -1;
139 } else {
140
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
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
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:
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
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
372
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
389
390 public MemoryBackend() {
391
392 }
393
394
395
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
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
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
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
456 }
457 } else {
458
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
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
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
511 newContent.remove(operation.getField());
512 } else {
513
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
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
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
584 final QueryFilter<JsonPointer> filter = request.getQueryFilter();
585
586
587
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
612
613 int resultIndex = 0;
614 int resultCount;
615 if (sortKeys.isEmpty()) {
616
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
629
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
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
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
715
716
717
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 }