-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathBaseEntity.java
504 lines (457 loc) · 20.3 KB
/
BaseEntity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.db.mixing;
import sirius.db.mixing.annotations.Transient;
import sirius.db.mixing.query.Query;
import sirius.db.mixing.query.constraints.Constraint;
import sirius.kernel.commons.Strings;
import sirius.kernel.di.std.Part;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.NLS;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Represents the base class for all entities which can be managed using {@link Mixing}.
* <p>
* Each field will become a property, unless it is annotated with {@link Transient}.
* <p>
* The framework highly encourages composition over inheritance. Therefore {@link Composite} fields will directly
* result in the equivalent properties required to store the fields declared there. Still inheritance might be
* useful and is fully supported for both, entities and composites.
* <p>
* What is not supported, is merging distinct subclasses into one table or other weird inheritance methods. Therefore,
* all superclasses should be abstract.
* <p>
* Additionally all <tt>Mixins</tt> {@link sirius.db.mixing.annotations.Mixin} will be used to add properties to the
* entity. This is especially useful to extend existing entities from within customizations.
*
* @param <I> the type of the ID used by subclasses
*/
public abstract class BaseEntity<I> extends Mixable implements Entity {
@Part
protected static Mixing mixing;
@Transient
protected Map<Property, Object> persistedData = new HashMap<>();
/**
* Contains the unique id of the entity.
* <p>
* The actual {@code id} field with corresponding {@code <I>} type is defined by the underlying implementations.
*/
public static final Mapping ID = Mapping.named("id");
/**
* Contains the constant used to mark a new (unsaved) entity.
*/
public static final String NEW = "new";
private static final String PARAM_FIELD = "field";
private static final Mapping[] EMPTY_MAPPINGS = new Mapping[0];
/**
* Returns the descriptor which maps the entity to the database table.
*
* @return the descriptor which is in charge of checking and mapping the entity to the database
*/
public EntityDescriptor getDescriptor() {
return mixing.getDescriptor(getClass());
}
/**
* Returns the id of the entity.
*
* @return the id of the entity
*/
@Nullable
public abstract I getId();
/**
* Determines if the entity is new (not yet written to the database).
* <p>
* This won't work in {@link sirius.db.mixing.annotations.AfterSave} handlers - use {@link #wasCreated()} instead.
*
* @return <tt>true</tt> if the entity has not been written to the database yet, <tt>false</tt> otherwise
*/
@Override
public boolean isNew() {
return getId() == null;
}
@Override
public String getTypeName() {
return Mixing.getNameForType(getClass());
}
@Override
public final String getUniqueName() {
if (isNew()) {
return "";
}
return Mixing.getUniqueName(getTypeName(), getId());
}
/**
* Provides the {@link BaseMapper mapper} which is used to actually manage the entity.
*
* @param <E> the entity type of the mapper
* @param <Q> the query type of the mapper
* @return the mapper which is in charge of this entity.
*/
public abstract <E extends BaseEntity<?>, C extends Constraint, Q extends Query<Q, E, C>> BaseMapper<E, C, Q> getMapper();
/**
* Determines if the given value in the given field is unique within the given side constraints.
*
* @param field the field to check
* @param value the value to be unique
* @param within the side constraints within the value must be unique
* @return <tt>true</tt> if the given field is unique, <tt>false</tt> otherwise
*/
public abstract boolean isUnique(Mapping field, Object value, Mapping... within);
/**
* Ensures that the given value in the given field is unique within the given side constraints.
*
* @param field the field to check
* @param value the value to be unique
* @param within the side constraints within the value must be unique
* @throws sirius.kernel.health.HandledException if the value isn't unique
*/
public void assertUnique(Mapping field, Object value, Mapping... within) {
if (!isUnique(field, value, within)) {
throw Exceptions.createHandled()
.error(new InvalidFieldException(field.toString()))
.withNLSKey("Property.fieldNotUnique")
.set(PARAM_FIELD, getDescriptor().getProperty(field).getFullLabel())
.set("value", fetchUserStringForProperty(getDescriptor().getProperty(field)))
.handle();
}
}
/**
* Ensures that the given value in the given field is unique within the given side constraints.
*
* @param field the field to check
* @param value the value to be unique
* @param within the side constraints within the value must be unique
* @param fieldsForMessage the side constraints which should be mentioned within the exception message
* @throws sirius.kernel.health.HandledException if the value isn't unique
*/
public void assertUnique(Mapping field,
Object value,
Collection<Mapping> within,
Collection<Mapping> fieldsForMessage) {
if (!isUnique(field, value, within.toArray(EMPTY_MAPPINGS))) {
String fieldValueSet = Stream.concat(Stream.of(field), fieldsForMessage.stream())
.map(mapping -> getDescriptor().getProperty(mapping.getName()))
.map(property -> property.getFullLabel() + ": " + fetchUserStringForProperty(
property))
.collect(Collectors.joining(", "));
throw Exceptions.createHandled()
.error(new InvalidFieldException(field.toString()))
.withNLSKey("Property.fieldNotUniqueException")
.set("entity", getDescriptor().getLabel())
.set("fieldValueSet", fieldValueSet)
.handle();
}
}
private String fetchUserStringForProperty(Property property) {
final String userString = NLS.toUserString(property.getValueForUserMessage(this));
return Strings.isEmpty(userString) ? NLS.get("Property.emptyValue") : userString;
}
/**
* Asserts that the given field is filled.
* <p>
* This can be used for conditional <tt>null</tt> checks.
*
* @param field the field to check
* @see Property#isConsideredNull(Object)
*/
public void assertNonNull(Mapping field) {
assertNonNull(field, getDescriptor().getProperty(field).getValue(this));
}
/**
* Asserts that the given field, containing the given value is filled.
* <p>
* This can be used for conditional <tt>null</tt> checks.
*
* @param field the field to check
* @param value the value to check. Note that even a "non-null" value here, might be considered null/empty on the
* database layer (e.g. <tt>sirius.db.mixing.properties.AmountProperty.isConsideredNull(Object)</tt>).
* @see Property#isConsideredNull(Object)
*/
public void assertNonNull(Mapping field, Object value) {
Property property = getDescriptor().getProperty(field);
if (property.isConsideredNull(value)) {
throwEmptyFieldError(field, property);
}
}
private void throwEmptyFieldError(Mapping field, Property property) {
throw Exceptions.createHandled()
.error(new InvalidFieldException(field.toString()))
.withNLSKey("Property.fieldNotNullable")
.set(PARAM_FIELD, property.getFullLabel())
.handle();
}
/**
* Asserts that the given field remains unchanged.
* <p>
* This can be used for conditions where a field cannot be updated after creation.
*
* @param field the field to check
*/
public void assertFieldUnchanged(Mapping field) {
if (!isNew() && isChanged(field)) {
Property property = getDescriptor().getProperty(field);
throw Exceptions.createHandled()
.error(new InvalidFieldException(field.toString()))
.withNLSKey("Property.fieldNotUpdatable")
.set(PARAM_FIELD, property.getFullLabel())
.handle();
}
}
/**
* Emits a validation warning if the given field is considered <tt>null</tt>.
*
* @param field the field to check
* @param validationWarningConsumer the consumer to be supplied with validation warnings
*/
public void validateNonNull(Mapping field, Consumer<String> validationWarningConsumer) {
validateNonNull(field, getDescriptor().getProperty(field).getValue(this), validationWarningConsumer);
}
/**
* Emits a validation warning if the given field with the given value is considered <tt>null</tt>.
*
* @param field the field to check
* @param value the value to check
* @param validationWarningConsumer the consumer to be supplied with validation warnings
* @see #assertNonNull(Mapping)
*/
public void validateNonNull(Mapping field, Object value, Consumer<String> validationWarningConsumer) {
Property property = getDescriptor().getProperty(field);
if (property.isConsideredNull(value)) {
emitEmptyFieldWarning(validationWarningConsumer, property);
}
}
private void emitEmptyFieldWarning(Consumer<String> validationWarningConsumer, Property property) {
validationWarningConsumer.accept(NLS.fmtr("Property.fieldNotNullable")
.set(PARAM_FIELD, property.getFullLabel())
.format());
}
@Override
public final String getIdAsString() {
if (isNew()) {
return NEW;
}
return String.valueOf(getId());
}
/**
* Determines the given {@link Mapping mappings} were changed in this {@link BaseEntity} since it was last
* fetched from the database.
* <p>
* If a property wears an {@link sirius.db.mixing.annotations.Trim} annotation or if "" and <tt>null</tt>
* should be considered equal, {@link Strings#areEqual(Object, Object)}
* or {@link Strings#areTrimmedEqual(Object, Object)} can be used as <tt>equalsFunction</tt>.
*
* @param mappingToCheck the columns to check whether they were changed
* @param equalsFunction the function which compares the current and the previously persisted value and returns
* <tt>true</tt> if they are equal or <tt>false</tt> otherwise
* @return <tt>true</tt> if the requested property changed, <tt>false</tt> otherwise
*/
public boolean isChanged(Mapping mappingToCheck, BiPredicate<? super Object, ? super Object> equalsFunction) {
return getDescriptor().isChanged(this, getDescriptor().getProperty(mappingToCheck), equalsFunction);
}
/**
* Determines if at least one of the given {@link Mapping}s were changed in this {@link BaseEntity} since it was last
* fetched from the database.
*
* @param mappingsToCheck the columns to check whether they were changed
* @return <tt>true</tt> if at least one column was changed, <tt>false</tt> otherwise
*/
public boolean isChanged(Mapping... mappingsToCheck) {
for (Mapping mapping : mappingsToCheck) {
if (getDescriptor().isChanged(this, getDescriptor().getProperty(mapping))) {
return true;
}
}
return false;
}
/**
* Determines if the entity has just been created.
* <p>
* This only works if called from a {@link sirius.db.mixing.annotations.AfterSave} handler, as moments later the persisted
* data gets updated.
*
* @return <tt>true</tt> if the entity has just been written to the database, <tt>false</tt> otherwise
*/
public boolean wasCreated() {
return isChanged(ID);
}
/**
* Provides a boilerplate way of only executing a lambda if the referenced mapping has changed.
* <p>
* This is most probably useful for deciding if a check in a {@link sirius.db.mixing.annotations.BeforeSave}
* handler should be executed or not.
*
* @param mappingToCheck the mapping to check
* @param codeToExecute the lambda to execute if the mapping has changed
* @see #ifChangedAndFilled(Mapping, Runnable)
*/
public void ifChanged(Mapping mappingToCheck, Runnable codeToExecute) {
if (isChanged(mappingToCheck)) {
codeToExecute.run();
}
}
/**
* Provides a boilerplate way of only executing a lambda if the referenced mapping has changed and contains a
* non-null value.
* <p>
* This is most probably useful for deciding if a check in a {@link sirius.db.mixing.annotations.BeforeSave}
* handler should be executed or not.
*
* @param mappingToCheck the mapping to check
* @param codeToExecute the lambda to execute if the mapping has changed
* @see #ifChangedAndFilled(Mapping, Runnable)
*/
public void ifChangedAndFilled(@Nonnull Mapping mappingToCheck, @Nonnull Runnable codeToExecute) {
checkIfChangedOrEmpty(mappingToCheck, codeToExecute, null);
}
protected void checkIfChangedOrEmpty(@Nonnull Mapping mappingToCheck,
@Nonnull Runnable check,
@Nullable Runnable emptyHandler) {
Property property = getDescriptor().getProperty(mappingToCheck);
Object propertyValue = property.getValue(this);
if (property.isConsideredNull(propertyValue)) {
if (emptyHandler != null) {
emptyHandler.run();
}
return;
}
if (Objects.equals(persistedData.get(property), propertyValue)) {
return;
}
check.run();
}
/**
* Provides a boilerplate way of executing a check if the requested field is changed and filled or otherwise emit an
* error if the field is empty.
*
* @param mappingToCheck the mapping to check
* @param codeToExecute the lambda to execute if the mapping has changed
* @throws sirius.kernel.health.HandledException if the field is empty
*/
public void verifyIfChangedFailIfEmpty(@Nonnull Mapping mappingToCheck, @Nonnull Runnable codeToExecute) {
checkIfChangedOrEmpty(mappingToCheck, codeToExecute, () -> {
Property property = getDescriptor().getProperty(mappingToCheck);
throwEmptyFieldError(mappingToCheck, property);
});
}
/**
* Provides a boilerplate way of executing a validation to ensure that the requested field is changed and filled
* or otherwise emit a validation warning if the field is empty.
*
* @param mappingToCheck the mapping to check
* @param validationConsumer the validation consumer which is passed into the
* {@link sirius.db.mixing.annotations.OnValidate} method.
* @param codeToExecute the lambda to execute if the mapping has changed
*/
public void validateIfChangedFailIfEmpty(@Nonnull Mapping mappingToCheck,
@Nonnull Consumer<String> validationConsumer,
@Nonnull Consumer<Consumer<String>> codeToExecute) {
checkIfChangedOrEmpty(mappingToCheck, () -> codeToExecute.accept(validationConsumer), () -> {
Property property = getDescriptor().getProperty(mappingToCheck);
emitEmptyFieldWarning(validationConsumer, property);
});
}
/**
* Outputs the persisted data for the given property.
* <p>
* The persisted data is the value which was/is present in the database and commonly compared to the current
* value in the entity to perform change tracking for differential updates and journaling.
*
* @param property the property to lookup
* @return the value which has been loaded from the database or <tt>null</tt> if either no value was present
* or if the property wasn't loaded
*/
@Nullable
public Object getPersistedValue(Property property) {
return persistedData.get(property);
}
/**
* Checks whether any {@link Mapping} of the current {@link BaseEntity} changed.
*
* @return <tt>true</tt> if at least one column was changed, <tt>false</tt> otherwise.
*/
public boolean isAnyMappingChanged() {
return getDescriptor().getProperties().stream().anyMatch(property -> getDescriptor().isChanged(this, property));
}
/**
* Checks whether any {@link Mapping} of the current {@link BaseEntity} changed.
* <p>
* If a property wears an {@link sirius.db.mixing.annotations.Trim} annotation or if "" and <tt>null</tt>
* should be considered equal, {@link Strings#areEqual(Object, Object)}
* or {@link Strings#areTrimmedEqual(Object, Object)} can be used as <tt>equalsFunction</tt>.
*
* @param equalsFunction the function which compares the current and the previously persisted value and returns
* <tt>true</tt> if they are equal or <tt>false</tt> otherwise
* @return <tt>true</tt> if at least one column was changed, <tt>false</tt> otherwise.
*/
public boolean isAnyMappingChanged(BiPredicate<? super Object, ? super Object> equalsFunction) {
return getDescriptor().getProperties()
.stream()
.anyMatch(property -> getDescriptor().isChanged(this, property, equalsFunction));
}
/**
* Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those
* provided by {@link java.util.HashMap}.
* <p>
* The hash code of an entity is based on its ID. If the entity is not written to the database yet, we use
* the hash code as computed by {@link Object#hashCode()}. This matches the behaviour of {@link #equals(Object)}.
*
* @return a hash code value for this object.
*/
@Override
public int hashCode() {
if (isNew()) {
// Return a hash code appropriate to the implementation of equals.
return super.hashCode();
}
return getId().hashCode();
}
/**
* Indicates whether some other object is "equal to" this one.
* <p>
* Equality of two entities is based on their type and ID. If an entity is not written to the database yet, we
* determine equality as computed by {@link Object#equals(Object)}. This matches the behaviour of
* {@link #hashCode()}.
*
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (this.getClass() != other.getClass()) {
return false;
}
BaseEntity<?> otherEntity = (BaseEntity<?>) other;
if (isNew()) {
return otherEntity.isNew() && super.equals(other);
}
return Strings.areEqual(getId(), otherEntity.getId());
}
@Override
public String toString() {
if (isNew()) {
return "new " + getClass().getSimpleName();
} else {
return getUniqueName();
}
}
}