• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yahoo / elide / #7463

30 Nov 2025 04:34AM UTC coverage: 84.413% (+0.004%) from 84.409%
#7463

Pull #3393

web-flow
Merge branch 'master' into dependabot/maven/logback.version-1.5.20
Pull Request #3393: Bump logback.version from 1.5.18 to 1.5.20

19745 of 23391 relevant lines covered (84.41%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.16
/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java
1
/*
2
 * Copyright 2018, Yahoo Inc.
3
 * Licensed under the Apache License, Version 2.0
4
 * See LICENSE file in project root for terms.
5
 */
6
package com.yahoo.elide.core;
7

8
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.CREATE;
9
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.DELETE;
10
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE;
11
import static com.yahoo.elide.core.dictionary.EntityBinding.EMPTY_BINDING;
12
import static com.yahoo.elide.core.dictionary.EntityDictionary.getType;
13
import static com.yahoo.elide.core.type.ClassType.COLLECTION_TYPE;
14

15
import com.yahoo.elide.annotation.Audit;
16
import com.yahoo.elide.annotation.CreatePermission;
17
import com.yahoo.elide.annotation.DeletePermission;
18
import com.yahoo.elide.annotation.LifeCycleHookBinding;
19
import com.yahoo.elide.annotation.NonTransferable;
20
import com.yahoo.elide.annotation.ReadPermission;
21
import com.yahoo.elide.annotation.UpdatePermission;
22
import com.yahoo.elide.core.audit.InvalidSyntaxException;
23
import com.yahoo.elide.core.audit.LogMessage;
24
import com.yahoo.elide.core.audit.LogMessageImpl;
25
import com.yahoo.elide.core.datastore.DataStoreIterable;
26
import com.yahoo.elide.core.datastore.DataStoreTransaction;
27
import com.yahoo.elide.core.dictionary.EntityBinding;
28
import com.yahoo.elide.core.dictionary.EntityDictionary;
29
import com.yahoo.elide.core.dictionary.RelationshipType;
30
import com.yahoo.elide.core.exceptions.BadRequestException;
31
import com.yahoo.elide.core.exceptions.ForbiddenAccessException;
32
import com.yahoo.elide.core.exceptions.InternalServerErrorException;
33
import com.yahoo.elide.core.exceptions.InvalidAttributeException;
34
import com.yahoo.elide.core.exceptions.InvalidEntityBodyException;
35
import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException;
36
import com.yahoo.elide.core.exceptions.InvalidValueException;
37
import com.yahoo.elide.core.filter.expression.AndFilterExpression;
38
import com.yahoo.elide.core.filter.expression.FilterExpression;
39
import com.yahoo.elide.core.filter.predicates.InPredicate;
40
import com.yahoo.elide.core.filter.visitors.VerifyFieldAccessFilterExpressionVisitor;
41
import com.yahoo.elide.core.request.Argument;
42
import com.yahoo.elide.core.request.Attribute;
43
import com.yahoo.elide.core.request.EntityProjection;
44
import com.yahoo.elide.core.request.Pagination;
45
import com.yahoo.elide.core.request.Sorting;
46
import com.yahoo.elide.core.security.ChangeSpec;
47
import com.yahoo.elide.core.security.obfuscation.IdObfuscator;
48
import com.yahoo.elide.core.security.permissions.ExpressionResult;
49
import com.yahoo.elide.core.security.visitors.CanPaginateVisitor;
50
import com.yahoo.elide.core.type.ClassType;
51
import com.yahoo.elide.core.type.Type;
52
import com.yahoo.elide.core.utils.coerce.CoerceUtil;
53
import com.yahoo.elide.jsonapi.JsonApiSettings;
54
import com.yahoo.elide.jsonapi.document.processors.WithMetadata;
55
import com.yahoo.elide.jsonapi.models.Data;
56
import com.yahoo.elide.jsonapi.models.Meta;
57
import com.yahoo.elide.jsonapi.models.Relationship;
58
import com.yahoo.elide.jsonapi.models.Resource;
59
import com.yahoo.elide.jsonapi.models.ResourceIdentifier;
60
import com.fasterxml.jackson.annotation.JsonIgnore;
61
import com.google.common.base.Preconditions;
62
import com.google.common.base.Predicates;
63
import com.google.common.collect.Sets;
64
import org.apache.commons.collections4.CollectionUtils;
65
import org.apache.commons.collections4.IterableUtils;
66
import org.apache.commons.lang3.StringUtils;
67

68
import lombok.NonNull;
69
import reactor.core.publisher.Flux;
70

71
import java.io.Serializable;
72
import java.lang.annotation.Annotation;
73
import java.util.ArrayList;
74
import java.util.Collection;
75
import java.util.Collections;
76
import java.util.Comparator;
77
import java.util.LinkedHashMap;
78
import java.util.LinkedHashSet;
79
import java.util.List;
80
import java.util.Map;
81
import java.util.Objects;
82
import java.util.Optional;
83
import java.util.Set;
84
import java.util.TreeMap;
85
import java.util.function.Function;
86
import java.util.function.Supplier;
87
import java.util.stream.Collectors;
88
import java.util.stream.Stream;
89

90
/**
91
 * Resource wrapper around Entity bean.
92
 *
93
 * @param <T> type of resource
94
 */
95
public class PersistentResource<T> implements com.yahoo.elide.core.security.PersistentResource<T> {
96
    public static final Set<String> ALL_FIELDS = null;
1✔
97
    public static final String CLASS_NO_FIELD = "";
98
    /**
99
     * The Dictionary.
100
     */
101
    protected final EntityDictionary dictionary;
102
    private final Type type;
103
    private final String typeName;
104
    private final ResourceLineage lineage;
105
    private final Optional<String> uuid;
106
    private final DataStoreTransaction transaction;
107
    private final RequestScope requestScope;
108
    /* Sort strings first by length then contents */
109
    private final Comparator<String> lengthFirstComparator = (string1, string2) -> {
1✔
110
        int diff = string1.length() - string2.length();
1✔
111
        return diff == 0 ? string1.compareTo(string2) : diff;
1✔
112
    };
113
    protected T obj;
114
    private int hashCode = 0;
1✔
115

116
    /**
117
     * Construct a new resource from the ID provided.
118
     *
119
     * @param obj                the obj
120
     * @param parent             the parent
121
     * @param id                 the id
122
     * @param parentRelationship The parent relationship traversed to this resource.
123
     * @param scope              the request scope
124
     */
125
    public PersistentResource(
126
            @NonNull T obj,
1✔
127
            PersistentResource parent,
128
            String parentRelationship,
129
            String id,
130
            @NonNull RequestScope scope
1✔
131
    ) {
1✔
132
        this.obj = obj;
1✔
133
        this.type = getType(obj);
1✔
134
        this.uuid = Optional.ofNullable(id);
1✔
135
        this.lineage = parent != null
1✔
136
                ? new ResourceLineage(parent.lineage, parent, parentRelationship)
1✔
137
                : new ResourceLineage();
1✔
138
        this.dictionary = scope.getDictionary();
1✔
139
        this.typeName = dictionary.getJsonAliasFor(type);
1✔
140
        this.transaction = scope.getTransaction();
1✔
141
        this.requestScope = scope;
1✔
142
        dictionary.initializeEntity(obj);
1✔
143
    }
1✔
144

145
    /**
146
     * Construct a new resource from the ID provided.
147
     *
148
     * @param obj   the obj
149
     * @param id    the id
150
     * @param scope the request scope
151
     */
152
    public PersistentResource(
153
            @NonNull T obj,
1✔
154
            String id,
155
            @NonNull RequestScope scope
1✔
156
    ) {
157
        this(obj, null, null, id, scope);
1✔
158
    }
1✔
159

160
    /**
161
     * Create a resource in the database.
162
     *
163
     * @param entityClass  the entity class
164
     * @param requestScope the request scope
165
     * @param uuid         the (optional) uuid
166
     * @param <T>          object type
167
     * @return persistent resource
168
     */
169
    public static <T> PersistentResource<T> createObject(
170
            Type<T> entityClass,
171
            RequestScope requestScope,
172
            Optional<String> uuid) {
173
        return createObject(null, null, entityClass, requestScope, uuid);
1✔
174
    }
175

176
    /**
177
     * Create a resource in the database.
178
     *
179
     * @param parent             - The immediate ancestor in the lineage or null if this is a root.
180
     * @param parentRelationship - The name of the parent relationship traversed to create this object.
181
     * @param entityClass        the entity class
182
     * @param requestScope       the request scope
183
     * @param uuid               the (optional) uuid
184
     * @param <T>                object type
185
     * @return persistent resource
186
     */
187
    public static <T> PersistentResource<T> createObject(
188
            PersistentResource<?> parent,
189
            String parentRelationship,
190
            Type<T> entityClass,
191
            RequestScope requestScope,
192
            Optional<String> uuid) {
193

194
        T obj = requestScope.getTransaction().createNewObject(entityClass, requestScope);
1✔
195

196
        String id = uuid.orElse(null);
1✔
197

198
        PersistentResource<T> newResource = new PersistentResource<>(obj, parent, parentRelationship, id, requestScope);
1✔
199

200
        //The ID must be assigned before we add it to the new resources set.  Persistent resource
201
        //hashcode and equals are only based on the ID/UUID & type.
202
        assignId(newResource, id);
1✔
203

204
        // Keep track of new resources for non-transferable resources
205
        requestScope.getNewPersistentResources().add(newResource);
1✔
206
        checkPermission(CreatePermission.class, newResource);
1✔
207

208
        newResource.auditClass(Audit.Action.CREATE, new ChangeSpec(newResource, null, null, newResource.getObject()));
1✔
209

210
        requestScope.publishLifecycleEvent(newResource, CREATE);
1✔
211

212
        requestScope.setUUIDForObject(newResource.type, id, newResource.getObject());
1✔
213

214
        // Initialize null ToMany collections
215
        requestScope.getDictionary().getRelationships(entityClass).stream()
1✔
216
                .filter(relationName -> newResource.getRelationshipType(relationName).isToMany()
1✔
217
                        && newResource.getValueUnchecked(relationName) == null)
1✔
218
                .forEach(relationName -> newResource.setValue(relationName, new LinkedHashSet<>()));
1✔
219

220
        newResource.markDirty();
1✔
221
        return newResource;
1✔
222
    }
223

224
    /**
225
     * Load an single entity from the DB.
226
     *
227
     * @param projection   What to load from the DB.
228
     * @param id           the id
229
     * @param requestScope the request scope
230
     * @param <T>          type of resource
231
     * @return resource persistent resource
232
     * @throws InvalidObjectIdentifierException the invalid object identifier exception
233
     */
234
    @SuppressWarnings("resource")
235
    @NonNull
236
    public static <T> PersistentResource<T> loadRecord(
237
            EntityProjection projection,
238
            String id,
239
            RequestScope requestScope
240
    ) throws InvalidObjectIdentifierException {
241
        Preconditions.checkNotNull(projection);
1✔
242
        Preconditions.checkNotNull(id);
1✔
243
        Preconditions.checkNotNull(requestScope);
1✔
244

245
        DataStoreTransaction tx = requestScope.getTransaction();
1✔
246
        EntityDictionary dictionary = requestScope.getDictionary();
1✔
247
        Type<?> loadClass = projection.getType();
1✔
248

249
        // Check the resource cache if exists
250
        Object obj = requestScope.getObjectById(loadClass, id);
1✔
251
        if (obj == null) {
1✔
252
            // try to load object
253
            Optional<FilterExpression> permissionFilter = getPermissionFilterExpression(loadClass,
1✔
254
                    requestScope, projection.getRequestedFields());
1✔
255

256
            projection = projection
1✔
257
                    .copyOf()
1✔
258
                    .filterExpression(permissionFilter.orElse(null))
1✔
259
                    .build();
1✔
260
            Serializable idOrEntityId;
261
            Type<?> entityIdType = dictionary.getEntityIdType(loadClass);
1✔
262
            if (entityIdType != null) {
1✔
263
                // If it is by entity id use it
264
                idOrEntityId = (Serializable) CoerceUtil.coerce(id, entityIdType);
×
265
            } else {
266
                Type<?> idType = dictionary.getIdType(loadClass);
1✔
267
                IdObfuscator idObfuscator = dictionary.getIdObfuscator();
1✔
268
                if (idObfuscator != null) {
1✔
269
                    // If an obfuscator is present use it to deobfuscate the id
270
                    try {
271
                        idOrEntityId = (Serializable) idObfuscator.deobfuscate(id, idType);
×
272
                    } catch (RuntimeException e) {
×
273
                        throw new InvalidValueException(
×
274
                                "Invalid identifier " + id + " for " + dictionary.getJsonAliasFor(loadClass), e);
×
275
                    }
×
276
                } else {
277
                    idOrEntityId = (Serializable) CoerceUtil.coerce(id, idType);
1✔
278
                }
279
            }
280

281
            obj = tx.loadObject(projection, idOrEntityId, requestScope);
1✔
282
            if (obj == null) {
1✔
283
                throw new InvalidObjectIdentifierException(id, dictionary.getJsonAliasFor(loadClass));
1✔
284
            }
285
        }
286

287
        PersistentResource<T> resource = new PersistentResource<>(
1✔
288
                (T) obj,
289
                requestScope.getUUIDFor(obj),
1✔
290
                requestScope);
291

292
        // No need to have read access for a newly created object
293
        if (!requestScope.getNewResources().contains(resource)) {
1✔
294
            resource.checkFieldAwarePermissions(ReadPermission.class, projection.getRequestedFields());
1✔
295
        }
296

297
        return resource;
1✔
298
    }
299

300
    /**
301
     * Get a FilterExpression parsed from FilterExpressionCheck.
302
     *
303
     * @param <T>             the type parameter
304
     * @param loadClass       the load class
305
     * @param requestScope    the request scope
306
     * @param requestedFields The set of requested fields
307
     * @return a FilterExpression defined by FilterExpressionCheck.
308
     */
309
    private static <T> Optional<FilterExpression> getPermissionFilterExpression(Type<T> loadClass,
310
                                                                                RequestScope requestScope,
311
                                                                                Set<String> requestedFields) {
312
        try {
313
            return requestScope.getPermissionExecutor().getReadPermissionFilter(loadClass, requestedFields);
1✔
314
        } catch (ForbiddenAccessException e) {
×
315
            return Optional.empty();
×
316
        }
317
    }
318

319
    /**
320
     * Load a collection from the datastore.
321
     *
322
     * @param projection   the projection to load
323
     * @param requestScope the request scope
324
     * @param ids          a list of object identifiers to optionally load.  Can be empty.
325
     * @return a filtered collection of resources loaded from the datastore.
326
     */
327
    public static Flux<PersistentResource> loadRecords(
328
            EntityProjection projection,
329
            List<String> ids,
330
            RequestScope requestScope) {
331

332
        Type<?> loadClass = projection.getType();
1✔
333
        Pagination pagination = projection.getPagination();
1✔
334
        Sorting sorting = projection.getSorting();
1✔
335

336
        FilterExpression filterExpression = projection.getFilterExpression();
1✔
337

338
        EntityDictionary dictionary = requestScope.getDictionary();
1✔
339

340
        DataStoreTransaction tx = requestScope.getTransaction();
1✔
341

342
        if (shouldSkipCollection(loadClass, ReadPermission.class, requestScope, projection.getRequestedFields())) {
1✔
343
            if (ids.isEmpty()) {
×
344
                return Flux.empty();
×
345
            }
346
            throw new InvalidObjectIdentifierException(ids.toString(), dictionary.getJsonAliasFor(loadClass));
×
347
        }
348

349
        Set<String> requestedFields = projection.getRequestedFields();
1✔
350

351
        if (pagination != null && !pagination.isDefaultInstance()
1✔
352
                && !CanPaginateVisitor.canPaginate(loadClass, dictionary, requestScope, requestedFields)) {
×
353
            throw new BadRequestException(String.format("Cannot paginate %s",
×
354
                    dictionary.getJsonAliasFor(loadClass)));
×
355
        }
356

357
        Set<PersistentResource> newResources = new LinkedHashSet<>();
1✔
358

359
        if (!ids.isEmpty()) {
1✔
360
            String typeAlias = dictionary.getJsonAliasFor(loadClass);
×
361
            newResources = requestScope.getNewPersistentResources().stream()
×
362
                    .filter(resource -> typeAlias.equals(resource.getTypeName())
×
363
                            && ids.contains(resource.getUUID().orElse("")))
×
364
                    .collect(Collectors.toCollection(LinkedHashSet::new));
×
365
            FilterExpression idExpression = buildIdFilterExpression(ids, loadClass, dictionary, requestScope);
×
366

367
            // Combine filters if necessary
368
            filterExpression = Optional.ofNullable(filterExpression)
×
369
                    .map(fe -> (FilterExpression) new AndFilterExpression(idExpression, fe))
×
370
                    .orElse(idExpression);
×
371
        }
372

373
        Optional<FilterExpression> permissionFilter = getPermissionFilterExpression(loadClass, requestScope,
1✔
374
                requestedFields);
375
        if (permissionFilter.isPresent()) {
1✔
376
            if (filterExpression != null) {
×
377
                filterExpression = new AndFilterExpression(filterExpression, permissionFilter.get());
×
378
            } else {
379
                filterExpression = permissionFilter.get();
×
380
            }
381
        }
382

383
        EntityProjection modifiedProjection = projection
1✔
384
                .copyOf()
1✔
385
                .filterExpression(filterExpression)
1✔
386
                .sorting(sorting)
1✔
387
                .pagination(pagination)
1✔
388
                .build();
1✔
389

390
        Flux<PersistentResource> existingResources = filter(
1✔
391
                ReadPermission.class,
392
                Optional.ofNullable(modifiedProjection.getFilterExpression()),
1✔
393
                projection.getRequestedFields(),
1✔
394
                Flux.fromIterable(
1✔
395
                        new PersistentResourceSet(tx.loadObjects(modifiedProjection, requestScope), requestScope))
1✔
396
        );
397

398
        // TODO: Sort again in memory now that two sets are glommed together?
399
        Flux<PersistentResource> allResources =
1✔
400
                Flux.fromIterable(newResources).mergeWith(existingResources);
1✔
401

402
        Set<String> foundIds = new LinkedHashSet<>();
1✔
403

404
        allResources = allResources.doOnNext((resource) -> {
1✔
405
            String id = (String) resource.getUUID().orElseGet(resource::getId);
1✔
406
            if (ids.contains(id)) {
1✔
407
                foundIds.add(id);
×
408
            }
409
        });
1✔
410

411
        allResources = allResources.doOnComplete(() -> {
1✔
412
            Set<String> missedIds = Sets.difference(new LinkedHashSet<>(ids), foundIds);
1✔
413
            if (!missedIds.isEmpty()) {
1✔
414
                throw new InvalidObjectIdentifierException(missedIds.toString(), dictionary.getJsonAliasFor(loadClass));
×
415
            }
416
        });
1✔
417

418
        return allResources;
1✔
419
    }
420

421
    /**
422
     * Build an id filter expression for a particular entity type.
423
     *
424
     * @param ids        Ids to include in the filter expression
425
     * @param entityType Type of entity
426
     * @return Filter expression for given ids and type.
427
     */
428
    private static FilterExpression buildIdFilterExpression(List<String> ids,
429
                                                            Type<?> entityType,
430
                                                            EntityDictionary dictionary,
431
                                                            RequestScope scope) {
432
        Type<?> entityIdType = dictionary.getEntityIdType(entityType);
1✔
433
        Type<?> idType;
434
        String idField;
435
        if (entityIdType != null) {
1✔
436
            idType = entityIdType;
×
437
            idField = dictionary.getEntityIdFieldName(entityType);
×
438
        } else {
439
            idType = dictionary.getIdType(entityType);
1✔
440
            idField = dictionary.getIdFieldName(entityType);
1✔
441
        }
442
        IdObfuscator idObfuscator = entityIdType != null ? null : dictionary.getIdObfuscator();
1✔
443
        List<Object> coercedIds = ids.stream()
1✔
444
                .filter(id -> scope.getObjectById(entityType, id) == null) // these don't exist yet
1✔
445
                .map(id -> idObfuscator == null ? CoerceUtil.coerce(id, idType) : idObfuscator.deobfuscate(id, idType))
1✔
446
                .collect(Collectors.toList());
1✔
447

448
        /* construct a new SQL like filter expression, eg: book.id IN [1,2] */
449
        FilterExpression idFilter = new InPredicate(
1✔
450
                new Path.PathElement(
451
                        entityType,
452
                        idType,
453
                        idField),
454
                coercedIds);
455

456
        return idFilter;
1✔
457
    }
458

459
    /**
460
     * Determine whether or not to skip loading a collection.
461
     *
462
     * @param resourceClass   Resource class
463
     * @param annotationClass Annotation class
464
     * @param requestedFields The set of requested fields
465
     * @param requestScope    Request scope
466
     * @return True if collection should be skipped (i.e. denied access), false otherwise
467
     */
468
    private static boolean shouldSkipCollection(Type<?> resourceClass, Class<? extends Annotation> annotationClass,
469
                                                RequestScope requestScope, Set<String> requestedFields) {
470
        try {
471
            requestScope.getPermissionExecutor().checkUserPermissions(resourceClass, annotationClass, requestedFields);
1✔
472
            return false;
1✔
473
        } catch (ForbiddenAccessException e) {
1✔
474
            return true;
1✔
475
        }
476
    }
477

478
    /**
479
     * Invoke the get[fieldName] method on the target object OR get the field with the corresponding name.
480
     *
481
     * @param target       the object to get
482
     * @param fieldName    the field name to get or invoke equivalent get method
483
     * @param requestScope the request scope
484
     * @return the value
485
     */
486
    public static Object getValue(Object target, String fieldName, RequestScope requestScope) {
487
        return requestScope.getDictionary().getValue(target, fieldName, requestScope);
1✔
488
    }
489

490
    /**
491
     * Filter a set of PersistentResources.
492
     * Verify fields have ReadPermission on filter join.
493
     *
494
     * @param permission the permission
495
     * @param resources  the resources
496
     * @return Filtered set of resources
497
     */
498
    protected static Flux<PersistentResource> filter(Class<? extends Annotation> permission,
499
                                                           Optional<FilterExpression> filter,
500
                                                           Set<String> requestedFields,
501
                                                           Flux<PersistentResource> resources) {
502

503
        return resources.filter(resource -> {
1✔
504
            try {
505
                // NOTE: This is for avoiding filtering on _newly created_ objects within this transaction.
506
                // Namely-- in a JSONPATCH request or GraphQL request-- we need to read all newly created
507
                // resources /regardless/ of whether or not we actually have permission to do so; this is to
508
                // retrieve the object id to return to the caller. If no fields on the object are readable by the caller
509
                // then they will be filtered out and only the id is returned. Similarly, all future requests to this
510
                // object will behave as expected.
511
                if (!resource.getRequestScope().getNewResources().contains(resource)) {
1✔
512
                    resource.checkFieldAwarePermissions(permission, requestedFields);
1✔
513
                    // Verify fields have ReadPermission on filter join
514
                    return !filter.isPresent()
1✔
515
                            || filter.get().accept(new VerifyFieldAccessFilterExpressionVisitor(resource));
1✔
516
                }
517
                return true;
×
518
            } catch (ForbiddenAccessException e) {
1✔
519
                return false;
1✔
520
            }
521
        });
522
    }
523

524
    private static <A extends Annotation> ExpressionResult checkPermission(
525
            Class<A> annotationClass, PersistentResource resource) {
526
        return resource.requestScope.getPermissionExecutor().checkPermission(annotationClass, resource);
1✔
527
    }
528

529
    private static <A extends Annotation> ExpressionResult checkUserPermission(
530
            Class<A> annotationClass, Object obj, RequestScope requestScope, Set<String> requestedFields) {
531
        return requestScope.getPermissionExecutor()
×
532
                .checkUserPermissions(getType(obj), annotationClass, requestedFields);
×
533
    }
534

535
    /**
536
     * Assign provided id if id field is not generated.
537
     *
538
     * @param persistentResource resource
539
     * @param id                 resource id
540
     */
541
    private static void assignId(PersistentResource persistentResource, String id) {
542

543
        //If id field is not a `@GeneratedValue` or mapped via a `@MapsId` attribute
544
        //then persist the provided id
545
        if (!persistentResource.isIdGenerated()) {
1✔
546
            if (StringUtils.isNotEmpty(id)) {
1✔
547
                persistentResource.setId(id);
1✔
548
            } else {
549
                //If expecting id to persist and id is not present, throw exception
550
                throw new BadRequestException(
×
551
                        "No id provided, cannot persist " + persistentResource.getTypeName());
×
552
            }
553
        }
554
        // Set the entity id if necessary
555
        if (StringUtils.isNotEmpty(id)) {
1✔
556
            persistentResource.setEntityId(id);
1✔
557
        }
558
    }
1✔
559

560
    private static <T> T firstOrNullIfEmpty(final Collection<T> coll) {
561
        return CollectionUtils.isEmpty(coll) ? null : IterableUtils.first(coll);
1✔
562
    }
563

564
    public static <T> T firstOrNullIfEmpty(final Flux<T> coll) {
565
        return firstOrNullIfEmpty(coll.collectList().block());
1✔
566
    }
567

568
    @Override
569
    public String toString() {
570
        return String.format("PersistentResource{type=%s, id=%s}", typeName, uuid.orElseGet(this::getId));
1✔
571
    }
572

573
    /**
574
     * Check whether an id matches for this persistent resource.
575
     *
576
     * @param checkId the check id
577
     * @return True if matches false otherwise
578
     */
579
    @Override
580
    public boolean matchesId(String checkId) {
581
        if (checkId == null) {
1✔
582
            return false;
×
583
        }
584
        return uuid
1✔
585
                .map(checkId::equals)
1✔
586
                .orElseGet(() -> {
1✔
587
                    String id = getId();
1✔
588
                    return !"0".equals(id) && !"null".equals(id) && checkId.equals(id);
1✔
589
                });
590
    }
591

592
    /**
593
     * Update attribute in existing resource.
594
     *
595
     * @param fieldName the field name
596
     * @param newVal    the new val
597
     * @return true if object updated, false otherwise
598
     */
599
    public boolean updateAttribute(String fieldName, Object newVal) {
600
        Type<?> fieldClass = dictionary.getType(getResourceType(), fieldName);
1✔
601
        final Object coercedNewValue = dictionary.coerce(obj, newVal, fieldName, fieldClass);
1✔
602
        Object val = getValueUnchecked(fieldName);
1✔
603
        checkFieldAwareDeferPermissions(UpdatePermission.class, fieldName, coercedNewValue, val);
1✔
604
        if (!Objects.equals(val, coercedNewValue)) {
1✔
605
            if (val == null
1✔
606
                    || coercedNewValue == null
607
                    || !dictionary.isComplexAttribute(getType(obj), fieldName)) {
1✔
608
                this.setValueChecked(fieldName, coercedNewValue);
1✔
609
            } else {
610
                if (newVal instanceof Map) {
1✔
611

612
                    //We perform a copy here for two reasons:
613
                    //1. We want the original so we can dispatch update life cycle hooks.
614
                    //2. Some stores (Hibernate) won't notice changes to an attribute if the attribute
615
                    //has a @TypeDef annotation unless we modify the reference in the parent object.  This rules
616
                    //out an update in place strategy.
617
                    Object copy = copyComplexAttribute(val);
1✔
618

619
                    //Update the copy.
620
                    this.updateComplexAttribute(dictionary, (Map<String, Object>) newVal, copy, requestScope);
1✔
621

622
                    //Set the copy.
623
                    dictionary.setValue(obj, fieldName, copy);
1✔
624
                    triggerUpdate(fieldName, val, copy);
1✔
625
                } else {
1✔
626
                    this.setValueChecked(fieldName, coercedNewValue);
×
627
                }
628
            }
629
            this.markDirty();
1✔
630
            //Hooks for customize logic for setAttribute/Relation
631
            if (dictionary.isAttribute(getType(obj), fieldName)) {
1✔
632
                transaction.setAttribute(obj, Attribute.builder()
1✔
633
                        .name(fieldName)
1✔
634
                        .type(fieldClass)
1✔
635
                        .argument(Argument.builder()
1✔
636
                                .name("_UNUSED_")
1✔
637
                                .value(newVal).build())
1✔
638
                        .build(), requestScope);
1✔
639
            }
640
            return true;
1✔
641
        }
642
        return false;
1✔
643
    }
644

645
    private void updateComplexAttribute(EntityDictionary dictionary,
646
                                        Map<String, Object> updateValue,
647
                                        Object currentValue,
648
                                        RequestScope scope) {
649
        for (String field : updateValue.keySet()) {
1✔
650
            final Object newValue = updateValue.get(field);
1✔
651
            final Object coercedNewValue =
1✔
652
                    dictionary.coerce(currentValue, newValue, field, dictionary.getType(currentValue, field));
1✔
653
            final Object newOriginal = dictionary.getValue(currentValue, field, scope);
1✔
654
            if (!Objects.equals(newOriginal, coercedNewValue)) {
1✔
655
                if (newOriginal == null
1✔
656
                        || coercedNewValue == null
657
                        || !dictionary.isComplexAttribute(ClassType.of(currentValue.getClass()), field)) {
1✔
658
                    dictionary.setValue(currentValue, field, coercedNewValue);
1✔
659
                } else {
660
                    if (newValue instanceof Map) {
×
661
                        this.updateComplexAttribute(dictionary, (Map<String, Object>) newValue, newOriginal, scope);
×
662
                    } else {
663
                        dictionary.setValue(currentValue, field, coercedNewValue);
×
664
                    }
665
                }
666
            }
667
        }
1✔
668
    }
1✔
669

670
    /**
671
     * Copies a complex attribute.  If the attribute fields are complex, recurses to perform a deep copy.
672
     * @param object The attribute to copy.
673
     * @return The copy.
674
     */
675
    private Object copyComplexAttribute(Object object) {
676
        if (object == null) {
1✔
677
            return null;
×
678
        }
679

680
        Type<?> type = getType(object);
1✔
681
        EntityBinding binding = dictionary.getEntityBinding(type);
1✔
682

683
        Preconditions.checkState(! binding.equals(EMPTY_BINDING), "Model not found.");
1✔
684
        Preconditions.checkState(binding.apiRelationships.isEmpty(), "Deep copy of relationships not supported");
1✔
685

686
        Object copy;
687
        try {
688
            copy = type.newInstance();
1✔
689
        } catch (InstantiationException | IllegalAccessException e) {
×
690
            throw new IllegalStateException("Cannot perform deep copy of " + type.getName(), e);
×
691
        }
1✔
692

693
        binding.apiAttributes.forEach(attribute -> {
1✔
694
            Object newValue;
695
            Object oldValue = dictionary.getValue(object, attribute, requestScope);
1✔
696
            if (! dictionary.isComplexAttribute(type, attribute)) {
1✔
697
                newValue = oldValue;
1✔
698
            } else {
699
                newValue = copyComplexAttribute(oldValue);
×
700
            }
701
            dictionary.setValue(copy, attribute, newValue);
1✔
702
        });
1✔
703

704
        return copy;
1✔
705
    }
706

707
    /**
708
     * Perform a full replacement on relationships.
709
     * Here is an example:
710
     * The following are ids for a hypothetical relationship.
711
     * GIVEN:
712
     * all (all the ids in the DB) = 1,2,3,4,5
713
     * mine (everything the current user has access to) = 1,2,3
714
     * requested (what the user wants to change to) = 3,6
715
     * THEN:
716
     * deleted (what gets removed from the DB) = 1,2
717
     * final (what get stored in the relationship) = 3,4,5,6
718
     * BECAUSE:
719
     * notMine = all - mine
720
     * updated = (requested UNION mine) - (requested INTERSECT mine)
721
     * deleted = (mine - requested)
722
     * final = (notMine) UNION requested
723
     *
724
     * @param fieldName           the field name
725
     * @param resourceIdentifiers the resource identifiers
726
     * @return True if object updated, false otherwise
727
     */
728
    public boolean updateRelation(String fieldName, Set<PersistentResource> resourceIdentifiers) {
729
        RelationshipType type = getRelationshipType(fieldName);
1✔
730

731
        Set<PersistentResource> resources = filter(
1✔
732
                ReadPermission.class,
733
                Optional.empty(),
1✔
734
                ALL_FIELDS,
735
                getRelationUncheckedUnfiltered(fieldName)).collect(Collectors.toCollection(LinkedHashSet::new)).block();
1✔
736

737
        boolean isUpdated;
738
        if (type.isToMany()) {
1✔
739
            List<Object> modifiedResources = CollectionUtils.isEmpty(resourceIdentifiers)
1✔
740
                    ? Collections.emptyList()
1✔
741
                    : resourceIdentifiers.stream().map(PersistentResource::getObject).collect(Collectors.toList());
1✔
742
            checkFieldAwareDeferPermissions(
1✔
743
                    UpdatePermission.class,
744
                    fieldName,
745
                    modifiedResources,
746
                    resources.stream().map(PersistentResource::getObject).collect(Collectors.toList())
1✔
747
            );
748
            isUpdated = updateToManyRelation(fieldName, resourceIdentifiers, resources);
1✔
749
        } else { // To One Relationship
1✔
750
            PersistentResource resource = firstOrNullIfEmpty(resources);
1✔
751
            Object original = (resource == null) ? null : resource.getObject();
1✔
752
            PersistentResource modifiedResource = firstOrNullIfEmpty(resourceIdentifiers);
1✔
753
            Object modified = (modifiedResource == null) ? null : modifiedResource.getObject();
1✔
754
            checkFieldAwareDeferPermissions(UpdatePermission.class, fieldName, modified, original);
1✔
755
            isUpdated = updateToOneRelation(fieldName, resourceIdentifiers, resources);
1✔
756
        }
757
        return isUpdated;
1✔
758
    }
759

760
    /**
761
     * Updates a to-many relationship.
762
     *
763
     * @param fieldName           the field name
764
     * @param resourceIdentifiers the resource identifiers
765
     * @param mine                Existing, filtered relationships for field name
766
     * @return true if updated. false otherwise
767
     */
768
    protected boolean updateToManyRelation(String fieldName,
769
                                           Set<PersistentResource> resourceIdentifiers,
770
                                           Set<PersistentResource> mine) {
771

772
        Set<PersistentResource> requested;
773
        Set<PersistentResource> updated;
774
        Set<PersistentResource> deleted;
775
        Set<PersistentResource> added;
776

777
        if (resourceIdentifiers == null) {
1✔
778
            throw new InvalidEntityBodyException("Bad relation data");
×
779
        }
780
        if (resourceIdentifiers.isEmpty()) {
1✔
781
            requested = new LinkedHashSet<>();
1✔
782
        } else {
783

784
            // TODO - this resource does not include a lineage. This could cause issues for audit.
785
            requested = resourceIdentifiers;
1✔
786
        }
787

788
        // deleted = mine - requested
789
        deleted = Sets.difference(mine, requested);
1✔
790

791
        // updated = (requested UNION mine) - (requested INTERSECT mine)
792
        updated = Sets.difference(
1✔
793
                Sets.union(mine, requested),
1✔
794
                Sets.intersection(mine, requested)
1✔
795
        );
796

797
        added = Sets.difference(updated, deleted);
1✔
798

799
        checkTransferablePermission(added);
1✔
800

801
        Set<Object> newRelationships = new LinkedHashSet<>();
1✔
802
        Set<Object> deletedRelationships = new LinkedHashSet<>();
1✔
803

804
        deleted
1✔
805
                .stream()
1✔
806
                .forEach(toDelete -> {
1✔
807
                    deletedRelationships.add(toDelete.getObject());
1✔
808
                });
1✔
809

810
        added
1✔
811
                .stream()
1✔
812
                .forEach(toAdd -> {
1✔
813
                    newRelationships.add(toAdd.getObject());
1✔
814
                });
1✔
815

816
        Collection collection = (Collection) this.getValueUnchecked(fieldName);
1✔
817
        modifyCollection(collection, fieldName, newRelationships, deletedRelationships, true);
1✔
818

819
        if (!updated.isEmpty()) {
1✔
820
            this.markDirty();
1✔
821
        }
822

823
        //hook for updateRelation
824
        transaction.updateToManyRelation(transaction, obj, fieldName,
1✔
825
                newRelationships, deletedRelationships, requestScope);
826

827
        return !updated.isEmpty();
1✔
828
    }
829

830
    /**
831
     * Update a 2-one relationship.
832
     *
833
     * @param fieldName           the field name
834
     * @param resourceIdentifiers the resource identifiers
835
     * @param mine                Existing, filtered relationships for field name
836
     * @return true if updated. false otherwise
837
     */
838
    protected boolean updateToOneRelation(String fieldName,
839
                                          Set<PersistentResource> resourceIdentifiers,
840
                                          Set<PersistentResource> mine) {
841
        Object newValue = null;
1✔
842
        PersistentResource newResource = null;
1✔
843
        if (CollectionUtils.isNotEmpty(resourceIdentifiers)) {
1✔
844
            newResource = IterableUtils.first(resourceIdentifiers);
1✔
845
            newValue = newResource.getObject();
1✔
846
        }
847

848
        PersistentResource oldResource = firstOrNullIfEmpty(mine);
1✔
849

850
        if (oldResource == null) {
1✔
851
            if (newValue == null) {
1✔
852
                return false;
×
853
            }
854
            checkTransferablePermission(resourceIdentifiers);
1✔
855
        } else if (oldResource.getObject().equals(newValue)) {
1✔
856
            return false;
1✔
857
        } else {
858
            checkTransferablePermission(resourceIdentifiers);
1✔
859
            if (hasInverseRelation(fieldName)) {
1✔
860
                deleteInverseRelation(fieldName, oldResource.getObject());
×
861
                oldResource.markDirty();
×
862
            }
863
        }
864

865
        if (newResource != null) {
1✔
866
            if (hasInverseRelation(fieldName)) {
1✔
867
                addInverseRelation(fieldName, newValue);
1✔
868
                newResource.markDirty();
1✔
869
            }
870
        }
871

872
        this.setValueChecked(fieldName, newValue);
1✔
873
        //hook for updateToOneRelation
874
        transaction.updateToOneRelation(transaction, obj, fieldName, newValue, requestScope);
1✔
875

876
        this.markDirty();
1✔
877
        return true;
1✔
878
    }
879

880
    /**
881
     * Clear all elements from a relation.
882
     *
883
     * @param relationName Name of relation to clear
884
     * @return True if object updated, false otherwise
885
     */
886
    public boolean clearRelation(String relationName) {
887
        Set<PersistentResource> mine = filter(ReadPermission.class, Optional.empty(),
1✔
888
                ALL_FIELDS,
889
                getRelationUncheckedUnfiltered(relationName)).collect(Collectors.toCollection(LinkedHashSet::new))
1✔
890
                .block();
1✔
891

892
        checkFieldAwareDeferPermissions(UpdatePermission.class, relationName, Collections.emptySet(),
1✔
893
                mine.stream().map(PersistentResource::getObject).collect(Collectors.toCollection(LinkedHashSet::new)));
1✔
894

895
        if (mine.isEmpty()) {
1✔
896
            return false;
1✔
897
        }
898

899
        RelationshipType type = getRelationshipType(relationName);
1✔
900

901
        if (type.isToOne()) {
1✔
902
            PersistentResource oldValue = IterableUtils.first(mine);
1✔
903
            if (oldValue != null && oldValue.getObject() != null) {
1✔
904
                this.nullValue(relationName, oldValue);
1✔
905
                oldValue.markDirty();
1✔
906
                this.markDirty();
1✔
907
                //hook for updateToOneRelation
908
                transaction.updateToOneRelation(transaction, obj, relationName, null, requestScope);
1✔
909

910
            }
911
        } else {
1✔
912
            Collection collection = (Collection) getValueUnchecked(relationName);
1✔
913
            if (CollectionUtils.isNotEmpty(collection)) {
1✔
914
                Set<Object> deletedRelationships = new LinkedHashSet<>();
1✔
915
                mine.stream()
1✔
916
                        .forEach(toDelete -> {
1✔
917
                            deletedRelationships.add(toDelete.getObject());
1✔
918
                        });
1✔
919
                modifyCollection(collection, relationName, Collections.emptySet(), deletedRelationships, true);
1✔
920
                this.markDirty();
1✔
921
                //hook for updateToManyRelation
922
                transaction.updateToManyRelation(transaction, obj, relationName,
1✔
923
                        new LinkedHashSet<>(), deletedRelationships, requestScope);
924

925
            }
926
        }
927
        return true;
1✔
928
    }
929

930
    /**
931
     * Remove a relationship.
932
     *
933
     * @param fieldName      the field name
934
     * @param removeResource the remove resource
935
     */
936
    public void removeRelation(String fieldName, PersistentResource removeResource) {
937
        Object relation = getValueUnchecked(fieldName);
1✔
938
        Object original = relation;
1✔
939
        Object modified = null;
1✔
940

941
        if (relation instanceof Collection) {
1✔
942
            original = copyCollection((Collection) relation);
1✔
943
        }
944

945
        if (relation instanceof Collection && removeResource != null) {
1✔
946
            modified = CollectionUtils.disjunction(
1✔
947
                    (Collection) relation,
948
                    Collections.singleton(removeResource.getObject())
1✔
949
            );
950
        }
951

952
        checkFieldAwareDeferPermissions(UpdatePermission.class, fieldName, modified, original);
1✔
953

954
        if (relation instanceof Collection) {
1✔
955
            if (removeResource == null || !((Collection) relation).contains(removeResource.getObject())) {
1✔
956

957
                //Nothing to do
958
                return;
1✔
959
            }
960
            modifyCollection((Collection) relation, fieldName, Collections.emptySet(),
1✔
961
                    Set.of(removeResource.getObject()), true);
1✔
962
        } else {
963
            if (relation == null || removeResource == null || !relation.equals(removeResource.getObject())) {
1✔
964
                //Nothing to do
965
                return;
1✔
966
            }
967
            this.nullValue(fieldName, removeResource);
1✔
968

969
            if (hasInverseRelation(fieldName)) {
1✔
970
                deleteInverseRelation(fieldName, removeResource.getObject());
×
971
                removeResource.markDirty();
×
972
            }
973
        }
974

975
        if (!Objects.equals(original, modified)) {
1✔
976
            this.markDirty();
1✔
977
        }
978

979
        RelationshipType type = getRelationshipType(fieldName);
1✔
980
        if (type.isToOne()) {
1✔
981
            //hook for updateToOneRelation
982
            transaction.updateToOneRelation(transaction, obj, fieldName, null, requestScope);
1✔
983
        } else {
984
            //hook for updateToManyRelation
985
            transaction.updateToManyRelation(transaction, obj, fieldName,
1✔
986
                    new LinkedHashSet<>(), Sets.newHashSet(removeResource.getObject()), requestScope);
1✔
987
        }
988
    }
1✔
989

990
    /**
991
     * Add relation link from a given parent resource to a child resource.
992
     *
993
     * @param fieldName   which relation link
994
     * @param newRelation the new relation
995
     */
996
    public void addRelation(String fieldName, PersistentResource newRelation) {
997
        checkTransferablePermission(Collections.singleton(newRelation));
1✔
998
        Object relation = this.getValueUnchecked(fieldName);
1✔
999

1000
        if (relation instanceof Collection) {
1✔
1001
            if (modifyCollection((Collection) relation, fieldName,
1✔
1002
                    Set.of(newRelation.getObject()), Collections.emptySet(), true)) {
1✔
1003
                this.markDirty();
1✔
1004

1005
                //Hook for updateToManyRelation
1006
                transaction.updateToManyRelation(transaction, obj, fieldName,
1✔
1007
                        Sets.newHashSet(newRelation.getObject()), new LinkedHashSet<>(), requestScope);
1✔
1008

1009
                addInverseRelation(fieldName, newRelation.getObject());
1✔
1010
            }
1011
        } else {
1012
            // Not a collection, but may be trying to create a ToOne relationship.
1013
            // NOTE: updateRelation marks dirty.
1014
            updateRelation(fieldName, Collections.singleton(newRelation));
1✔
1015
        }
1016
    }
1✔
1017

1018
    /**
1019
     * Check if adding or updating a relation is allowed.
1020
     *
1021
     * @param resourceIdentifiers The persistent resources that are being added
1022
     */
1023
    protected void checkTransferablePermission(Set<PersistentResource> resourceIdentifiers) {
1024
        if (resourceIdentifiers == null) {
1✔
1025
            return;
1✔
1026
        }
1027

1028
        final Set<PersistentResource> newResources = getRequestScope().getNewPersistentResources();
1✔
1029

1030
        for (PersistentResource persistentResource : resourceIdentifiers) {
1✔
1031

1032
            //New resources are exempt from NonTransferable checks
1033
            if (newResources.contains(persistentResource)
1✔
1034
                    //This allows nested object hierarchies of non-transferables that are created in more than one
1035
                    //client request. A & B are created in one request and C is created in a subsequent request).
1036
                    //Even though B is non-transferable, while creating C in /A/B, C is allowed to
1037
                    //reference B because B & C are part of the same non-transferable object hierarchy.
1038
                    //To do this, the client must be able to read B (since they navigated through it) and also
1039
                    //update the relationship that links B & C.
1040

1041
                    //The object being added (C) is non-transferable
1042
                    || (!dictionary.isTransferable(getResourceType())
1✔
1043
                    //The object being added to (B) is not strict
1044
                    && !dictionary.isStrictNonTransferable(persistentResource.getResourceType())
1✔
1045
                    //B is in C's lineage (/B/C).
1046
                    && persistentResource.equals(lineage.getParent()))) {
1✔
1047
                continue;
1✔
1048
            }
1049

1050
            checkPermission(NonTransferable.class, persistentResource);
1✔
1051
        }
1✔
1052
    }
1✔
1053

1054
    /**
1055
     * Delete an existing entity.
1056
     *
1057
     * @throws ForbiddenAccessException the forbidden access exception
1058
     */
1059
    public void deleteResource() throws ForbiddenAccessException {
1060
        checkPermission(DeletePermission.class, this);
1✔
1061
        /*
1062
         * Search for bidirectional relationships.  For each bidirectional relationship,
1063
         * we need to remove ourselves from that relationship
1064
         */
1065

1066
        Type<?> resourceClass = getResourceType();
1✔
1067
        List<String> relationships = dictionary.getRelationships(resourceClass);
1✔
1068
        for (String relationName : relationships) {
1✔
1069

1070
            /* Skip updating inverse relationships for deletes which are cascaded */
1071
            if (dictionary.cascadeDeletes(resourceClass, relationName)) {
1✔
1072
                continue;
1✔
1073
            }
1074
            String inverseRelationName = dictionary.getRelationInverse(resourceClass, relationName);
1✔
1075
            if (!"".equals(inverseRelationName)) {
1✔
1076
                for (PersistentResource inverseResource : getRelationUncheckedUnfiltered(relationName)
1✔
1077
                        .collectList().block()) {
1✔
1078
                    if (hasInverseRelation(relationName)) {
1✔
1079
                        deleteInverseRelation(relationName, inverseResource.getObject());
1✔
1080
                        inverseResource.markDirty();
1✔
1081
                    }
1082
                }
1✔
1083
            }
1084
        }
1✔
1085

1086
        transaction.delete(getObject(), requestScope);
1✔
1087
        auditClass(Audit.Action.DELETE, new ChangeSpec(this, null, getObject(), null));
1✔
1088
        requestScope.publishLifecycleEvent(this, DELETE);
1✔
1089
        requestScope.getDeletedResources().add(this);
1✔
1090
    }
1✔
1091

1092
    /**
1093
     * Get resource ID.
1094
     *
1095
     * @return ID id
1096
     */
1097
    @Override
1098
    public String getId() {
1099
        return dictionary.getId(getObject());
1✔
1100
    }
1101

1102
    /**
1103
     * Set resource ID.
1104
     *
1105
     * @param id resource id
1106
     */
1107
    public void setId(String id) {
1108
        dictionary.setId(obj, id);
1✔
1109
    }
1✔
1110

1111
    /**
1112
     * Indicates if the ID is generated or not.
1113
     *
1114
     * @return Boolean
1115
     */
1116
    public boolean isIdGenerated() {
1117
        return dictionary.getEntityBinding(type).isIdGenerated();
1✔
1118
    }
1119

1120
    /**
1121
     * Set entity ID.
1122
     *
1123
     * @param entityId entity id
1124
     */
1125
    public void setEntityId(String entityId) {
1126
        dictionary.setEntityId(obj, entityId);
1✔
1127
    }
1✔
1128

1129
    /**
1130
     * Gets UUID.
1131
     *
1132
     * @return the UUID
1133
     */
1134
    @Override
1135
    public Optional<String> getUUID() {
1136
        return uuid;
1✔
1137
    }
1138

1139
    /**
1140
     * Get relation looking for a _single_ id.
1141
     * <p>
1142
     * NOTE: Filter expressions for this type are _not_ applied at this level.
1143
     *
1144
     * @param relationship The relationship
1145
     * @param id           single id to lookup
1146
     * @return The PersistentResource of the sought id or null if does not exist.
1147
     */
1148
    public PersistentResource getRelation(com.yahoo.elide.core.request.Relationship relationship, String id) {
1149
        List<PersistentResource> resources =
1✔
1150
                getRelation(Collections.singletonList(id), relationship).collectList().block();
1✔
1151

1152
        if (resources.isEmpty()) {
1✔
1153
            return null;
×
1154
        }
1155
        // If this is an in-memory object (i.e. UUID being created within tx), datastore may not be able to filter.
1156
        // If we get multiple results back, make sure we find the right id first.
1157
        for (PersistentResource resource : resources) {
1✔
1158
            if (resource.matchesId(id)) {
1✔
1159
                return resource;
1✔
1160
            }
1161
        }
×
1162
        return null;
×
1163
    }
1164

1165
    /**
1166
     * Load a relation from the PersistentResource.
1167
     *
1168
     * @param relationship the relation
1169
     * @param ids          a list of object identifiers to optionally load.  Can be empty.
1170
     * @return PersistentResource relation
1171
     */
1172
    public Flux<PersistentResource> getRelation(List<String> ids,
1173
                                                      com.yahoo.elide.core.request.Relationship relationship) {
1174

1175
        FilterExpression filterExpression = Optional.ofNullable(relationship.getProjection().getFilterExpression())
1✔
1176
                .orElse(null);
1✔
1177

1178
        assertPropertyExists(relationship.getName());
1✔
1179
        Type<?> entityType = dictionary.getParameterizedType(getResourceType(), relationship.getName());
1✔
1180

1181
        Set<PersistentResource> newResources = new LinkedHashSet<>();
1✔
1182

1183
        /* If this is a bulk edit request and the ID we are fetching for is newly created... */
1184
        if (!ids.isEmpty()) {
1✔
1185
            // Fetch our set of new resources that we know about since we can't find them in the datastore
1186
            newResources = requestScope.getNewPersistentResources().stream()
1✔
1187
                    .filter(resource -> entityType.isAssignableFrom(resource.getResourceType())
1✔
1188
                            && ids.contains(resource.getUUID().orElse("")))
×
1189
                    .collect(Collectors.toCollection(LinkedHashSet::new));
1✔
1190

1191
            FilterExpression idExpression = buildIdFilterExpression(ids, entityType, dictionary, requestScope);
1✔
1192

1193
            // Combine filters if necessary
1194
            filterExpression = Optional.ofNullable(relationship.getProjection().getFilterExpression())
1✔
1195
                    .map(fe -> (FilterExpression) new AndFilterExpression(idExpression, fe))
1✔
1196
                    .orElse(idExpression);
1✔
1197
        }
1198

1199
        // TODO: Filter on new resources?
1200
        // TODO: Update pagination to subtract the number of new resources created?
1201

1202
        Flux<PersistentResource> existingResources = filter(
1✔
1203
                ReadPermission.class,
1204
                Optional.ofNullable(filterExpression),
1✔
1205
                relationship.getProjection().getRequestedFields(),
1✔
1206
                getRelation(relationship.copyOf()
1✔
1207
                        .projection(relationship.getProjection().copyOf()
1✔
1208
                                .filterExpression(filterExpression)
1✔
1209
                                .build())
1✔
1210
                        .build(), true));
1✔
1211

1212
        // TODO: Sort again in memory now that two sets are glommed together?
1213
        Flux<PersistentResource> allResources =
1✔
1214
                Flux.fromIterable(newResources).mergeWith(existingResources);
1✔
1215

1216
        Set<String> foundIds = new LinkedHashSet<>();
1✔
1217

1218
        allResources = allResources.doOnNext((resource) -> {
1✔
1219
            String id = (String) (resource.getUUID().orElseGet(resource::getId));
1✔
1220
            if (ids.contains(id)) {
1✔
1221
                foundIds.add(id);
1✔
1222
            }
1223
        });
1✔
1224

1225
        allResources = allResources.doOnComplete(() -> {
1✔
1226
            Set<String> missedIds = Sets.difference(new LinkedHashSet<>(ids), foundIds);
1✔
1227
            if (!missedIds.isEmpty()) {
1✔
1228
                throw new InvalidObjectIdentifierException(missedIds.toString(), relationship.getName());
1✔
1229
            }
1230
        });
1✔
1231

1232
        return allResources;
1✔
1233
    }
1234

1235
    /**
1236
     * Get observable of resources from relation field.
1237
     *
1238
     * @param relationship relationship
1239
     * @return collection relation
1240
     */
1241
    public Flux<PersistentResource> getRelationCheckedFiltered(
1242
            com.yahoo.elide.core.request.Relationship relationship) {
1243
        return filter(ReadPermission.class,
1✔
1244
                Optional.ofNullable(relationship.getProjection().getFilterExpression()),
1✔
1245
                relationship.getProjection().getRequestedFields(),
1✔
1246
                getRelation(relationship, true));
1✔
1247
    }
1248

1249
    private Flux<PersistentResource> getRelationUncheckedUnfiltered(String relationName) {
1250
        assertPropertyExists(relationName);
1✔
1251
        return getRelation(com.yahoo.elide.core.request.Relationship.builder()
1✔
1252
                .name(relationName)
1✔
1253
                .alias(relationName)
1✔
1254
                .projection(EntityProjection.builder()
1✔
1255
                        .type(dictionary.getParameterizedType(getResourceType(), relationName))
1✔
1256
                        .build())
1✔
1257
                .build(), false);
1✔
1258
    }
1259

1260
    private void assertPropertyExists(String propertyName) {
1261
        if (propertyName == null || dictionary.getParameterizedType(obj, propertyName) == null) {
1✔
1262
            throw new InvalidAttributeException(propertyName, this.getTypeName());
1✔
1263
        }
1264
    }
1✔
1265

1266
    private Flux<PersistentResource> getRelation(com.yahoo.elide.core.request.Relationship relationship,
1267
                                                       boolean checked) {
1268

1269
        if (checked && !checkRelation(relationship)) {
1✔
1270
            return Flux.empty();
1✔
1271
        }
1272

1273
        Type<?> relationClass = dictionary.getParameterizedType(obj, relationship.getName());
1✔
1274

1275
        Optional<Pagination> pagination = Optional.ofNullable(relationship.getProjection().getPagination());
1✔
1276

1277
        if (pagination.filter(Predicates.not(Pagination::isDefaultInstance)).isPresent()
1✔
1278
                && !CanPaginateVisitor.canPaginate(
×
1279
                relationClass,
1280
                dictionary,
1281
                requestScope,
1282
                relationship.getProjection().getRequestedFields())) {
×
1283

1284
            throw new BadRequestException(String.format("Cannot paginate %s",
×
1285
                    dictionary.getJsonAliasFor(relationClass)));
×
1286
        }
1287

1288
        return getRelationUnchecked(relationship);
1✔
1289
    }
1290

1291
    /**
1292
     * Check the permissions of the relationship, and return true or false.
1293
     *
1294
     * @param relationship The relationship to the entity
1295
     * @return True if the relationship to the entity has valid permissions for the user
1296
     */
1297
    protected boolean checkRelation(com.yahoo.elide.core.request.Relationship relationship) {
1298
        String relationName = relationship.getName();
1✔
1299

1300
        String realName = dictionary.getNameFromAlias(obj, relationName);
1✔
1301
        relationName = (realName == null) ? relationName : realName;
1✔
1302

1303
        assertPropertyExists(relationName);
1✔
1304

1305
        checkFieldAwareDeferPermissions(ReadPermission.class, relationName, null, null);
1✔
1306

1307
        return !shouldSkipCollection(
1✔
1308
                dictionary.getParameterizedType(obj, relationName),
1✔
1309
                ReadPermission.class,
1310
                requestScope,
1311
                relationship.getProjection().getRequestedFields());
1✔
1312
    }
1313

1314
    /**
1315
     * Get collection of resources from relation field.  Does not filter the relationship and does
1316
     * not invoke lifecycle hooks.
1317
     *
1318
     * @param relationship the relationship to fetch
1319
     * @return collection relation
1320
     */
1321
    public Flux<PersistentResource> getRelationChecked(com.yahoo.elide.core.request.Relationship relationship) {
1322
        if (!checkRelation(relationship)) {
1✔
1323
            return Flux.empty();
×
1324
        }
1325
        return getRelationUnchecked(relationship);
1✔
1326
    }
1327

1328
    /**
1329
     * Retrieve an unchecked set of relations.
1330
     */
1331
    private Flux<PersistentResource> getRelationUnchecked(
1332
            com.yahoo.elide.core.request.Relationship relationship) {
1333
        String relationName = relationship.getName();
1✔
1334
        FilterExpression filterExpression = relationship.getProjection().getFilterExpression();
1✔
1335
        Pagination pagination = relationship.getProjection().getPagination();
1✔
1336
        Sorting sorting = relationship.getProjection().getSorting();
1✔
1337

1338
        RelationshipType type = getRelationshipType(relationName);
1✔
1339
        final Type<?> relationClass = dictionary.getParameterizedType(obj, relationName);
1✔
1340
        if (relationClass == null) {
1✔
1341
            throw new InvalidAttributeException(relationName, this.getTypeName());
×
1342
        }
1343

1344
        //Invoke filterExpressionCheck and then merge with filterExpression.
1345
        Optional<FilterExpression> permissionFilter = getPermissionFilterExpression(relationClass,
1✔
1346
                requestScope, relationship.getProjection().getRequestedFields());
1✔
1347
        Optional<FilterExpression> computedFilters = Optional.ofNullable(filterExpression);
1✔
1348

1349
        if (permissionFilter.isPresent() && filterExpression != null) {
1✔
1350
            FilterExpression mergedExpression =
×
1351
                    new AndFilterExpression(filterExpression, permissionFilter.get());
×
1352
            computedFilters = Optional.of(mergedExpression);
×
1353
        } else if (permissionFilter.isPresent()) {
1✔
1354
            computedFilters = permissionFilter;
×
1355
        }
1356

1357
        com.yahoo.elide.core.request.Relationship modifiedRelationship = relationship.copyOf()
1✔
1358
                .projection(relationship.getProjection().copyOf()
1✔
1359
                        .filterExpression(computedFilters.orElse(null))
1✔
1360
                        .sorting(sorting)
1✔
1361
                        .pagination(pagination)
1✔
1362
                        .build()
1✔
1363
                ).build();
1✔
1364

1365
        Flux<PersistentResource> resources;
1366

1367
        if (type.isToMany()) {
1✔
1368
            DataStoreIterable val = transaction.getToManyRelation(transaction, obj, modifiedRelationship, requestScope);
1✔
1369

1370
            if (val == null) {
1✔
1371
                return Flux.empty();
1✔
1372
            }
1373
            resources = Flux.fromIterable(
1✔
1374
                    new PersistentResourceSet(this, relationName, val, requestScope));
1375
        } else {
1✔
1376
            Object val = transaction.getToOneRelation(transaction, obj, modifiedRelationship, requestScope);
1✔
1377
            if (val == null) {
1✔
1378
                return Flux.empty();
1✔
1379
            }
1380
            resources = Flux.just(new PersistentResource(val, this, relationName,
1✔
1381
                    requestScope.getUUIDFor(val), requestScope));
1✔
1382
        }
1383

1384
        return resources;
1✔
1385
    }
1386

1387
    /**
1388
     * Get a relationship type.
1389
     *
1390
     * @param relation Name of relationship
1391
     * @return Relationship type. RelationshipType.NONE if not found.
1392
     */
1393
    public RelationshipType getRelationshipType(String relation) {
1394
        return dictionary.getRelationshipType(obj, relation);
1✔
1395
    }
1396

1397
    /**
1398
     * Get the value for a particular attribute (i.e. non-relational field)
1399
     *
1400
     * @param attr Attribute name
1401
     * @return Object value for attribute
1402
     */
1403
    @Deprecated
1404
    public Object getAttribute(String attr) {
1405
        assertPropertyExists(attr);
1✔
1406

1407
        return this.getAttribute(
1✔
1408
                Attribute.builder()
1✔
1409
                        .name(attr)
1✔
1410
                        .alias(attr)
1✔
1411
                        .type(dictionary.getParameterizedType(getResourceType(), attr))
1✔
1412
                        .build());
1✔
1413
    }
1414

1415
    /**
1416
     * Get the value for a particular attribute (i.e. non-relational field)
1417
     *
1418
     * @param attr the Attribute
1419
     * @return Object value for attribute
1420
     */
1421
    public Object getAttribute(Attribute attr) {
1422
        return this.getValueChecked(attr);
1✔
1423
    }
1424

1425
    /**
1426
     * Wrapped Entity bean.
1427
     *
1428
     * @return bean object
1429
     */
1430
    @Override
1431
    public T getObject() {
1432
        return obj;
1✔
1433
    }
1434

1435
    /**
1436
     * Sets object.
1437
     *
1438
     * @param obj the obj
1439
     */
1440
    public void setObject(T obj) {
1441
        this.obj = obj;
×
1442
    }
×
1443

1444
    /**
1445
     * Entity type.
1446
     *
1447
     * @return type resource class
1448
     */
1449
    @Override
1450
    @JsonIgnore
1451
    public Type<T> getResourceType() {
1452
        return (Type) dictionary.lookupBoundClass(getType(obj));
1✔
1453
    }
1454

1455
    /**
1456
     * Gets type.
1457
     *
1458
     * @return the type
1459
     */
1460
    @Override
1461
    public String getTypeName() {
1462
        return typeName;
1✔
1463
    }
1464

1465
    @Override
1466
    public int hashCode() {
1467
        if (hashCode == 0) {
1✔
1468
            // NOTE: UUID's are only present in the case of newly created objects.
1469
            // Consequently, a known ID will never be present during processing (only after commit
1470
            // assigned by the DB) and so we can assume that any newly created object can be fully
1471
            // addressed by its UUID. It is possible for UUID and id to be unset upon a POST or PATCH
1472
            // ext request, but it is safe to ignore these edge cases.
1473
            //     (1) In a POST request, you would not be referencing this newly created object in any way
1474
            //         so this is not an issue.
1475
            //     (2) In a PATCH ext request, this is also acceptable (assuming request is accepted) in the way
1476
            //         that it is acceptable in a POST. If you do not specify a UUID, there is no way to reference
1477
            //         that newly created object within the context of the request. Thus, if any such action was
1478
            //         required, the user would be forced to provide a UUID anyway.
1479
            String id = dictionary.getId(getObject());
1✔
1480
            if (uuid.isPresent() && ("0".equals(id) || "null".equals(id))) {
1✔
1481
                hashCode = Objects.hashCode(uuid);
1✔
1482
            } else {
1483
                hashCode = Objects.hashCode(id);
1✔
1484
            }
1485
        }
1486
        return hashCode;
1✔
1487
    }
1488

1489
    @Override
1490
    public boolean equals(Object obj) {
1491
        if (obj instanceof PersistentResource) {
1✔
1492
            PersistentResource that = (PersistentResource) obj;
1✔
1493
            if (this.getObject() == that.getObject()) {
1✔
1494
                return true;
1✔
1495
            }
1496
            String theirId = dictionary.getId(that.getObject());
1✔
1497
            return this.matchesId(theirId) && Objects.equals(this.typeName, that.typeName);
1✔
1498
        }
1499
        return false;
1✔
1500
    }
1501

1502
    /**
1503
     * Returns whether or not this resource was created in this transaction.
1504
     *
1505
     * @return True if this resource is newly created.
1506
     */
1507
    public boolean isNewlyCreated() {
1508
        return requestScope.getNewResources().contains(this);
1✔
1509
    }
1510

1511
    /**
1512
     * Gets lineage.
1513
     *
1514
     * @return the lineage
1515
     */
1516
    public ResourceLineage getLineage() {
1517
        return this.lineage;
1✔
1518
    }
1519

1520
    /**
1521
     * Gets dictionary.
1522
     *
1523
     * @return the dictionary
1524
     */
1525
    public EntityDictionary getDictionary() {
1526
        return dictionary;
1✔
1527
    }
1528

1529
    /**
1530
     * Gets request scope.
1531
     *
1532
     * @return the request scope
1533
     */
1534
    @Override
1535
    public RequestScope getRequestScope() {
1536
        return requestScope;
1✔
1537
    }
1538

1539
    /**
1540
     * Convert a persistent resource to a resource.
1541
     *
1542
     * @return a resource
1543
     */
1544
    public Resource toResource() {
1545
        return toResource(this::getRelationships, this::getAttributes);
1✔
1546
    }
1547

1548
    /**
1549
     * Fetch a resource with support for lambda function for getting relationships and attributes.
1550
     *
1551
     * @return The Resource
1552
     */
1553
    public Resource toResource(EntityProjection projection) {
1554
        return toResource(() -> getRelationships(projection), this::getAttributes);
1✔
1555
    }
1556

1557
    /**
1558
     * Fetch a resource with support for lambda function for getting relationships and attributes.
1559
     *
1560
     * @param relationshipSupplier The relationship supplier (getRelationships())
1561
     * @param attributeSupplier    The attribute supplier
1562
     * @return The Resource
1563
     */
1564
    private Resource toResource(final Supplier<Map<String, Relationship>> relationshipSupplier,
1565
                                final Supplier<Map<String, Object>> attributeSupplier) {
1566
        return toResource(relationshipSupplier.get(), attributeSupplier.get());
1✔
1567
    }
1568

1569
    /**
1570
     * Convert a persistent resource to a resource.
1571
     *
1572
     * @param relationships The relationships
1573
     * @param attributes    The attributes
1574
     * @return The Resource
1575
     */
1576
    public Resource toResource(final Map<String, Relationship> relationships,
1577
                               final Map<String, Object> attributes) {
1578
        final Resource resource = new Resource(typeName, (obj == null)
1✔
1579
                ? uuid.orElseThrow(
×
1580
                () -> new InvalidEntityBodyException("No id found on object"))
×
1581
                : dictionary.getId(obj));
1✔
1582
        resource.setRelationships(relationships);
1✔
1583
        resource.setAttributes(attributes);
1✔
1584

1585
        JsonApiSettings jsonApiSettings = requestScope.getElideSettings().getSettings(JsonApiSettings.class);
1✔
1586
        if (jsonApiSettings != null && jsonApiSettings.getLinks().isEnabled()) {
1✔
1587
            resource.setLinks(jsonApiSettings.getLinks().getJsonApiLinks().getResourceLevelLinks(this));
1✔
1588
        }
1589

1590
        if (! (getObject() instanceof WithMetadata)) {
1✔
1591
            return resource;
1✔
1592
        }
1593

1594
        WithMetadata withMetadata = (WithMetadata) getObject();
1✔
1595
        Set<String> fields = withMetadata.getMetadataFields();
1✔
1596

1597
        if (fields.size() == 0) {
1✔
1598
            return resource;
1✔
1599
        }
1600

1601
        Meta meta = new Meta(new LinkedHashMap<>());
1✔
1602

1603
        for (String field : fields) {
1✔
1604
            meta.getMetaMap().put(field, withMetadata.getMetadataField(field).get());
1✔
1605
        }
1✔
1606

1607
        resource.setMeta(meta);
1✔
1608

1609
        return resource;
1✔
1610
    }
1611

1612
    /**
1613
     * Get relationship mappings.
1614
     *
1615
     * @return Relationship mapping
1616
     */
1617
    protected Map<String, Relationship> getRelationships() {
1618
        return getRelationshipsWithRelationshipFunction((relationName) -> {
1✔
1619
            Optional<FilterExpression> filterExpression = requestScope.getExpressionForRelation(getResourceType(),
1✔
1620
                    relationName);
1621

1622
            return getRelationCheckedFiltered(com.yahoo.elide.core.request.Relationship.builder()
1✔
1623
                    .alias(relationName)
1✔
1624
                    .name(relationName)
1✔
1625
                    .projection(EntityProjection.builder()
1✔
1626
                            .type(dictionary.getParameterizedType(getResourceType(), relationName))
1✔
1627
                            .filterExpression(filterExpression.orElse(null))
1✔
1628
                            .build())
1✔
1629
                    .build());
1✔
1630
        });
1631
    }
1632

1633
    /**
1634
     * Get relationship mappings.
1635
     *
1636
     * @return Relationship mapping
1637
     */
1638
    private Map<String, Relationship> getRelationships(EntityProjection projection) {
1639
        return getRelationshipsWithRelationshipFunction(
1✔
1640
                (relationName) -> getRelationCheckedFiltered(projection.getRelationship(relationName)
1✔
1641
                        .orElseThrow(IllegalStateException::new)
1✔
1642
                ));
1643
    }
1644

1645
    /**
1646
     * Get relationship mappings.
1647
     *
1648
     * @param relationshipFunction a function to load the value of a relationship. Takes a string of the relationship
1649
     *                             name and returns the relationship's value.
1650
     * @return Relationship mapping
1651
     */
1652
    protected Map<String, Relationship> getRelationshipsWithRelationshipFunction(
1653
            final Function<String, Flux<PersistentResource>> relationshipFunction) {
1654
        final Map<String, Relationship> relationshipMap = new LinkedHashMap<>();
1✔
1655
        final Set<String> relationshipFields = filterFields(dictionary.getRelationships(obj));
1✔
1656

1657
        for (String field : relationshipFields) {
1✔
1658
            TreeMap<String, Resource> orderedById = new TreeMap<>(lengthFirstComparator);
1✔
1659
            for (PersistentResource relationship : relationshipFunction.apply(field).collectList().block()) {
1✔
1660
                orderedById.put(relationship.getId(),
1✔
1661
                        new ResourceIdentifier(relationship.getTypeName(), relationship.getId()).castToResource());
1✔
1662

1663
            }
1✔
1664
            Flux<Resource> resources = Flux.fromIterable(orderedById.values());
1✔
1665

1666
            Data<Resource> data;
1667
            RelationshipType relationshipType = getRelationshipType(field);
1✔
1668
            if (relationshipType.isToOne()) {
1✔
1669
                data = new Data<>(firstOrNullIfEmpty(resources));
1✔
1670
            } else {
1671
                data = new Data<>(resources);
1✔
1672
            }
1673
            Map<String, String> links = null;
1✔
1674
            JsonApiSettings jsonApiSettings = requestScope.getElideSettings().getSettings(JsonApiSettings.class);
1✔
1675
            if (jsonApiSettings != null && jsonApiSettings.getLinks().isEnabled()) {
1✔
1676
                links = jsonApiSettings.getLinks().getJsonApiLinks().getRelationshipLinks(this, field);
1✔
1677
            }
1678
            relationshipMap.put(field, new Relationship(links, data));
1✔
1679
        }
1✔
1680

1681
        return relationshipMap;
1✔
1682
    }
1683

1684
    /**
1685
     * Get attributes mapping from entity.
1686
     *
1687
     * @return Mapping of attributes to objects
1688
     */
1689
    protected Map<String, Object> getAttributes() {
1690
        final Map<String, Object> attributes = new LinkedHashMap<>();
1✔
1691

1692
        final Set<String> attrFields = filterFields(dictionary.getAttributes(obj));
1✔
1693
        for (String field : attrFields) {
1✔
1694
            Object val = getAttribute(field);
1✔
1695
            attributes.put(field, val);
1✔
1696
        }
1✔
1697
        return attributes;
1✔
1698
    }
1699

1700
    /**
1701
     * Sets value.
1702
     *
1703
     * @param fieldName the field name
1704
     * @param newValue  the new value
1705
     */
1706
    protected void setValueChecked(String fieldName, Object newValue) {
1707
        Object existingValue = getValueUnchecked(fieldName);
1✔
1708

1709
        // TODO: Need to refactor this logic. For creates this is properly converted in the executor. This logic
1710
        // should be explicitly encapsulated here, not there.
1711
        checkFieldAwareDeferPermissions(UpdatePermission.class, fieldName, newValue, existingValue);
1✔
1712

1713
        setValue(fieldName, newValue);
1✔
1714
    }
1✔
1715

1716
    /**
1717
     * Nulls the relationship or attribute and checks update permissions.
1718
     * Invokes the set[fieldName] method on the target object OR set the field with the corresponding name.
1719
     *
1720
     * @param fieldName the field name to set or invoke equivalent set method
1721
     * @param oldValue  the old value
1722
     */
1723
    protected void nullValue(String fieldName, PersistentResource oldValue) {
1724
        if (oldValue == null) {
1✔
1725
            return;
×
1726
        }
1727
        String inverseField = getInverseRelationField(fieldName);
1✔
1728
        if (!inverseField.isEmpty()) {
1✔
1729
            oldValue.checkFieldAwareDeferPermissions(UpdatePermission.class, inverseField, null, getObject());
1✔
1730
        }
1731
        this.setValueChecked(fieldName, null);
1✔
1732
    }
1✔
1733

1734
    /**
1735
     * Gets a value from an entity and checks read permissions.
1736
     *
1737
     * @param attribute the attribute to fetch.
1738
     * @return value value
1739
     */
1740
    protected Object getValueChecked(Attribute attribute) {
1741
        checkFieldAwareDeferPermissions(ReadPermission.class, attribute.getName(), null, null);
1✔
1742
        return transaction.getAttribute(getObject(), attribute, requestScope);
1✔
1743
    }
1744

1745
    /**
1746
     * Retrieve an object without checking read permissions (i.e. value is used internally and not sent to others)
1747
     *
1748
     * @param fieldName the field name
1749
     * @return Value
1750
     */
1751
    protected Object getValueUnchecked(String fieldName) {
1752
        return getValue(getObject(), fieldName, requestScope);
1✔
1753
    }
1754

1755
    protected boolean modifyCollection(
1756
            Collection toModify,
1757
            String collectionName,
1758
            Collection toAdd,
1759
            Collection toRemove,
1760
            boolean updateInverse) {
1761

1762
        Collection copyOfOriginal = copyCollection(toModify);
1✔
1763

1764
        Collection modified = CollectionUtils.union(CollectionUtils.emptyIfNull(toModify), toAdd);
1✔
1765
        modified = CollectionUtils.subtract(modified, toRemove);
1✔
1766

1767
        checkFieldAwareDeferPermissions(
1✔
1768
                UpdatePermission.class,
1769
                collectionName,
1770
                modified,
1771
                copyOfOriginal);
1772

1773
        if (updateInverse) {
1✔
1774
            for (Object adding : toAdd) {
1✔
1775
                addInverseRelation(collectionName, adding);
1✔
1776
            }
1✔
1777

1778
            for (Object removing : toRemove) {
1✔
1779
                deleteInverseRelation(collectionName, removing);
1✔
1780
            }
1✔
1781
        }
1782

1783
        if (toModify == null) {
1✔
1784
            this.setValueChecked(collectionName, modified);
1✔
1785
            return true;
1✔
1786
        } else {
1787
            if (copyOfOriginal.equals(modified)) {
1✔
1788
                return false;
1✔
1789
            }
1790
            toModify.addAll(toAdd);
1✔
1791
            toModify.removeAll(toRemove);
1✔
1792

1793
            triggerUpdate(collectionName, copyOfOriginal, modified);
1✔
1794
            return true;
1✔
1795
        }
1796
    }
1797

1798
    /**
1799
     * Invoke the set[fieldName] method on the target object OR set the field with the corresponding name.
1800
     *
1801
     * @param fieldName the field name to set or invoke equivalent set method
1802
     * @param value     the value to set
1803
     */
1804
    protected void setValue(String fieldName, Object value) {
1805
        final Object original = getValueUnchecked(fieldName);
1✔
1806

1807
        dictionary.setValue(obj, fieldName, value);
1✔
1808

1809
        triggerUpdate(fieldName, original, value);
1✔
1810
    }
1✔
1811

1812
    /**
1813
     * If a bidirectional relationship exists, attempts to delete itself from the inverse
1814
     * relationship. Given A to B as the relationship, A corresponds to this and B is the inverse.
1815
     *
1816
     * @param relationName  The name of the relationship on this (A) object.
1817
     * @param inverseEntity The value (B) which has been deleted from this object.
1818
     */
1819
    protected void deleteInverseRelation(String relationName, Object inverseEntity) {
1820
        String inverseField = getInverseRelationField(relationName);
1✔
1821

1822
        if (!"".equals(inverseField)) {
1✔
1823
            Type<?> inverseType = dictionary.getType(inverseEntity, inverseField);
1✔
1824

1825
            String uuid = requestScope.getUUIDFor(inverseEntity);
1✔
1826
            PersistentResource inverseResource = new PersistentResource(inverseEntity,
1✔
1827
                    this, relationName, uuid, requestScope);
1828
            Object inverseRelation = inverseResource.getValueUnchecked(inverseField);
1✔
1829

1830
            if (inverseRelation == null) {
1✔
1831
                return;
×
1832
            }
1833

1834
            if (inverseRelation instanceof Collection) {
1✔
1835
                inverseResource.modifyCollection((Collection) inverseRelation, inverseField,
1✔
1836
                        Collections.emptySet(), Set.of(this.getObject()), false);
1✔
1837
            } else if (inverseType.isAssignableFrom(this.getResourceType())) {
1✔
1838
                inverseResource.nullValue(inverseField, this);
1✔
1839
            } else {
1840
                throw new InternalServerErrorException("Relationship type mismatch");
×
1841
            }
1842
            inverseResource.markDirty();
1✔
1843

1844
            RelationshipType inverseRelationType = inverseResource.getRelationshipType(inverseField);
1✔
1845
            if (inverseRelationType.isToOne()) {
1✔
1846
                //hook for updateToOneRelation
1847
                transaction.updateToOneRelation(transaction, inverseEntity, inverseField, null, requestScope);
1✔
1848
            } else {
1849
                //hook for updateToManyRelation
1850
                assert (inverseRelation instanceof Collection) : inverseField + " not a collection";
1✔
1851
                transaction.updateToManyRelation(transaction, inverseEntity, inverseField,
1✔
1852
                        new LinkedHashSet<>(), Sets.newHashSet(obj), requestScope);
1✔
1853
            }
1854
        }
1855
    }
1✔
1856

1857
    private boolean hasInverseRelation(String relationName) {
1858
        String inverseField = getInverseRelationField(relationName);
1✔
1859
        return StringUtils.isNotEmpty(inverseField);
1✔
1860
    }
1861

1862
    private String getInverseRelationField(String relationName) {
1863
        return dictionary.getRelationInverse(type, relationName);
1✔
1864
    }
1865

1866
    /**
1867
     * If a bidirectional relationship exists, attempts to add itself to the inverse
1868
     * relationship. Given A to B as the relationship, A corresponds to this and B is the inverse.
1869
     *
1870
     * @param relationName The name of the relationship on this (A) object.
1871
     * @param inverseObj   The value (B) which has been added to this object.
1872
     */
1873
    protected void addInverseRelation(String relationName, Object inverseObj) {
1874
        String inverseName = dictionary.getRelationInverse(type, relationName);
1✔
1875

1876
        if (!"".equals(inverseName)) {
1✔
1877
            Type<?> inverseType = dictionary.getType(inverseObj, inverseName);
1✔
1878

1879
            String uuid = requestScope.getUUIDFor(inverseObj);
1✔
1880
            PersistentResource inverseResource = new PersistentResource(inverseObj,
1✔
1881
                    this, relationName, uuid, requestScope);
1882
            Object inverseRelation = inverseResource.getValueUnchecked(inverseName);
1✔
1883

1884
            if (COLLECTION_TYPE.isAssignableFrom(inverseType)) {
1✔
1885
                if (inverseRelation != null) {
1✔
1886
                    inverseResource.modifyCollection((Collection) inverseRelation, inverseName,
1✔
1887
                            Set.of(this.getObject()), Collections.emptySet(), false);
1✔
1888
                } else {
1889
                    inverseResource.setValueChecked(inverseName, Collections.singleton(this.getObject()));
×
1890
                }
1891
            } else if (inverseType.isAssignableFrom(this.getResourceType())) {
1✔
1892
                inverseResource.setValueChecked(inverseName, this.getObject());
1✔
1893
            } else {
1894
                throw new InternalServerErrorException("Relationship type mismatch");
×
1895
            }
1896
            inverseResource.markDirty();
1✔
1897

1898
            RelationshipType inverseRelationType = inverseResource.getRelationshipType(inverseName);
1✔
1899
            if (inverseRelationType.isToOne()) {
1✔
1900
                //hook for updateToOneRelation
1901
                transaction.updateToOneRelation(transaction, inverseObj, inverseName,
1✔
1902
                        obj, requestScope);
1903
            } else {
1904
                //hook for updateToManyRelation
1905
                assert (inverseRelation == null || inverseRelation instanceof Collection)
1✔
1906
                        : inverseName + " not a collection";
1907
                transaction.updateToManyRelation(transaction, inverseObj, inverseName,
1✔
1908
                        Sets.newHashSet(obj), new LinkedHashSet<>(), requestScope);
1✔
1909
            }
1910
        }
1911
    }
1✔
1912

1913
    /**
1914
     * Filter a set of fields.
1915
     *
1916
     * @param fields the fields
1917
     * @return Filtered set of fields
1918
     */
1919
    protected Set<String> filterFields(Collection<String> fields) {
1920
        Set<String> filteredSet = new LinkedHashSet<>();
1✔
1921
        Map<String, Set<String>> sparseFields = requestScope.getSparseFields();
1✔
1922
        Stream<String> stream;
1923
        if (sparseFields.isEmpty()) {
1✔
1924
            // If sparse fields is not set return all fields
1925
            stream = fields.stream();
1✔
1926
        } else {
1927
            // If sparse fields is set return those fields
1928
            Set<String> byType = sparseFields.get(typeName);
1✔
1929
            if (byType == null || fields == null || byType.isEmpty() || fields.isEmpty()) {
1✔
1930
                stream = Stream.empty();
×
1931
            } else {
1932
                stream = byType.stream().filter(fields::contains);
1✔
1933
            }
1934
        }
1935
        stream.forEach(field -> {
1✔
1936
            try {
1937
                checkFieldAwareReadPermissions(field);
1✔
1938
                filteredSet.add(field);
1✔
1939
            } catch (ForbiddenAccessException e) {
1✔
1940
                // Do nothing. Filter from set.
1941
            }
1✔
1942
        });
1✔
1943
        return filteredSet;
1✔
1944
    }
1945

1946
    /**
1947
     * Queue the @*Update triggers iff this is not a newly created object (otherwise we run @*Create).
1948
     */
1949
    private void triggerUpdate(String fieldName, Object original, Object value) {
1950
        ChangeSpec changeSpec = new ChangeSpec(this, fieldName, original, value);
1✔
1951
        LifeCycleHookBinding.Operation action = isNewlyCreated()
1✔
1952
                ? CREATE
1✔
1953
                : UPDATE;
1✔
1954

1955
        requestScope.publishLifecycleEvent(this, fieldName, action, Optional.of(changeSpec));
1✔
1956
        requestScope.publishLifecycleEvent(this, action);
1✔
1957
        auditField(new ChangeSpec(this, fieldName, original, value));
1✔
1958
    }
1✔
1959

1960
    private <A extends Annotation> ExpressionResult checkFieldAwarePermissions(
1961
            Class<A> annotationClass,
1962
            Set<String> requestedFields
1963
    ) {
1964
        return requestScope.getPermissionExecutor().checkPermission(annotationClass, this, requestedFields);
1✔
1965
    }
1966

1967
    private <A extends Annotation> ExpressionResult checkFieldAwareReadPermissions(String fieldName) {
1968
        return requestScope.getPermissionExecutor()
1✔
1969
                .checkSpecificFieldPermissions(this, null, ReadPermission.class, fieldName);
1✔
1970
    }
1971

1972
    private <A extends Annotation> ExpressionResult checkFieldAwareDeferPermissions(Class<A> annotationClass,
1973
                                                                                    String fieldName,
1974
                                                                                    Object modified,
1975
                                                                                    Object original) {
1976
        ChangeSpec changeSpec = (UpdatePermission.class.isAssignableFrom(annotationClass))
1✔
1977
                ? new ChangeSpec(this, fieldName, original, modified)
1✔
1978
                : null;
1✔
1979

1980
        return requestScope
1✔
1981
                .getPermissionExecutor()
1✔
1982
                .checkSpecificFieldPermissionsDeferred(this, changeSpec, annotationClass, fieldName);
1✔
1983
    }
1984

1985
    /**
1986
     * Audit an action on field.
1987
     *
1988
     * @param changeSpec Change spec for audit
1989
     */
1990
    protected void auditField(final ChangeSpec changeSpec) {
1991
        final String fieldName = changeSpec.getFieldName();
1✔
1992
        Audit[] annotations = dictionary.getAttributeOrRelationAnnotations(getResourceType(),
1✔
1993
                Audit.class,
1994
                fieldName
1995
        );
1996

1997
        if (annotations == null || annotations.length == 0) {
1✔
1998
            // Default to class-level annotation for action
1999
            auditClass(Audit.Action.UPDATE, changeSpec);
1✔
2000
            return;
1✔
2001
        }
2002
        for (Audit annotation : annotations) {
1✔
2003
            if (annotation.action().length == 1 && annotation.action()[0] == Audit.Action.UPDATE) {
1✔
2004
                LogMessage message = new LogMessageImpl(annotation, this, Optional.of(changeSpec));
1✔
2005
                getRequestScope().getAuditLogger().log(message);
1✔
2006
            } else {
1✔
2007
                throw new InvalidSyntaxException("Only Audit.Action.UPDATE is allowed on fields.");
×
2008
            }
2009
        }
2010
    }
1✔
2011

2012
    /**
2013
     * Audit an action on an entity.
2014
     *
2015
     * @param action     the action
2016
     * @param changeSpec the change that occurred
2017
     */
2018
    protected void auditClass(Audit.Action action, ChangeSpec changeSpec) {
2019
        Audit[] annotations = getResourceType().getAnnotationsByType(Audit.class);
1✔
2020

2021
        if (annotations == null) {
1✔
2022
            return;
×
2023
        }
2024
        for (Audit annotation : annotations) {
1✔
2025
            for (Audit.Action auditAction : annotation.action()) {
1✔
2026
                if (auditAction == action) { // compare object reference
1✔
2027
                    LogMessage message = new LogMessageImpl(annotation, this, Optional.ofNullable(changeSpec));
1✔
2028
                    getRequestScope().getAuditLogger().log(message);
1✔
2029
                }
2030
            }
2031
        }
2032
    }
1✔
2033

2034
    /**
2035
     * Shallow copy a collection.
2036
     *
2037
     * @param collection Collection to copy
2038
     * @return New copy of collection
2039
     */
2040
    private Collection copyCollection(final Collection collection) {
2041
        final ArrayList newCollection = new ArrayList();
1✔
2042
        if (CollectionUtils.isEmpty(collection)) {
1✔
2043
            return newCollection;
1✔
2044
        }
2045
        collection.iterator().forEachRemaining(newCollection::add);
1✔
2046
        return newCollection;
1✔
2047
    }
2048

2049
    /**
2050
     * Mark this object as dirty.
2051
     */
2052
    private void markDirty() {
2053
        requestScope.getDirtyResources().add(this);
1✔
2054
    }
1✔
2055
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc