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

kit-data-manager / ro-crate-java / #442

21 May 2025 02:55PM UTC coverage: 90.716% (+1.4%) from 89.357%
#442

Pull #255

github

Pfeil
refactor: change mergeIdIntoValue method visibility to protected and add unit tests
Pull Request #255: Next Version (v2.1.0-rc2)

271 of 291 new or added lines in 26 files covered. (93.13%)

1964 of 2165 relevant lines covered (90.72%)

0.91 hits per line

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

86.71
/src/main/java/edu/kit/datamanager/ro_crate/entities/AbstractEntity.java
1
package edu.kit.datamanager.ro_crate.entities;
2

3
import com.fasterxml.jackson.annotation.JsonIgnore;
4
import com.fasterxml.jackson.annotation.JsonUnwrapped;
5
import com.fasterxml.jackson.databind.JsonNode;
6
import com.fasterxml.jackson.databind.ObjectMapper;
7
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
8
import com.fasterxml.jackson.databind.node.ArrayNode;
9
import com.fasterxml.jackson.databind.node.ObjectNode;
10

11
import edu.kit.datamanager.ro_crate.entities.data.RootDataEntity;
12
import edu.kit.datamanager.ro_crate.entities.serializers.ObjectNodeSerializer;
13
import edu.kit.datamanager.ro_crate.entities.validation.EntityValidation;
14
import edu.kit.datamanager.ro_crate.entities.validation.JsonSchemaValidation;
15
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;
16
import edu.kit.datamanager.ro_crate.payload.Observer;
17
import edu.kit.datamanager.ro_crate.special.JsonUtilFunctions;
18
import edu.kit.datamanager.ro_crate.special.IdentifierUtils;
19

20
import java.util.*;
21
import java.util.regex.Matcher;
22
import java.util.regex.Pattern;
23

24
/**
25
 * Abstract Entity parent class of every singe item in the json metadata file.
26
 *
27
 * @author Nikola Tzotchev on 3.2.2022 г.
28
 * @version 1
29
 */
30
public class AbstractEntity {
31

32
    /**
33
     * This set contains the types of an entity (ex. File, Dataset, ect.) It is
34
     * a set because it does not make sense to have duplicates
35
     */
36
    @JsonIgnore
37
    private Set<String> types;
38

39
    /**
40
     * Contains the whole list of properties of the entity It uses a custom
41
     * serializer because of cases where a single array element should be
42
     * displayed as a single value. ex: "key" : ["value"]
43
     * <=> "key" : "value"
44
     */
45
    @JsonUnwrapped
46
    @JsonSerialize(using = ObjectNodeSerializer.class)
47
    private ObjectNode properties;
48

49
    private static final EntityValidation entityValidation
1✔
50
            = new EntityValidation(new JsonSchemaValidation());
51

52
    @JsonIgnore
53
    private final Set<String> linkedTo;
54

55
    @JsonIgnore
56
    private final Set<Observer> observers;
57

58
    public void addObserver(Observer observer) {
59
        this.observers.add(observer);
1✔
60
    }
1✔
61

62
    private void notifyObservers() {
63
        this.observers.forEach(observer -> observer.update(this.getId()));
1✔
64
    }
1✔
65

66
    /**
67
     * Constructor that takes a builder and instantiates all the fields from it.
68
     *
69
     * @param entityBuilder the entity builder passed to the constructor.
70
     */
71
    public AbstractEntity(AbstractEntityBuilder<?> entityBuilder) {
1✔
72
        this.types = entityBuilder.types;
1✔
73
        this.properties = entityBuilder.properties;
1✔
74
        this.linkedTo = entityBuilder.relatedItems;
1✔
75
        this.observers = new HashSet<>();
1✔
76
        if (this.properties.get("@id") == null) {
1✔
77
            if (entityBuilder.id == null) {
1✔
78
                this.properties.put("@id", UUID.randomUUID().toString());
1✔
79
            } else {
80
                this.properties.put("@id", entityBuilder.id);
1✔
81
            }
82
        }
83
    }
1✔
84

85
    public Set<String> getLinkedTo() {
86
        return linkedTo;
1✔
87
    }
88

89
    /**
90
     * Returns the types of this entity.
91
     *
92
     * @return a set of type strings.
93
     */
94
    public Set<String> getTypes() {
95
        return types;
1✔
96
    }
97

98
    /**
99
     * Returns a Json object containing the properties of the entity.
100
     *
101
     * @return ObjectNode representing the properties.
102
     */
103
    public ObjectNode getProperties() {
104
        if (this.types != null) {
1✔
105
            JsonNode node = MyObjectMapper.getMapper().valueToTree(this.types);
1✔
106
            this.properties.set("@type", node);
1✔
107
        }
108
        return properties;
1✔
109
    }
110

111
    public JsonNode getProperty(String propertyKey) {
112
        return this.properties.get(propertyKey);
1✔
113
    }
114

115
    @JsonIgnore
116
    public String getId() {
117
        JsonNode id = this.properties.get("@id");
1✔
118
        return id == null ? null : id.asText();
1✔
119
    }
120

121
    /**
122
     * Set all the properties from a Json object to the Entity. The entities are
123
     * first validated to filter any invalid entity properties.
124
     *
125
     * @param obj the object that contains all the json properties that should
126
     * be added.
127
     */
128
    public void setProperties(JsonNode obj) {
129
        // validate whole entity
130
        if (entityValidation.entityValidation(obj)) {
1✔
131
            this.properties = obj.deepCopy();
1✔
132
            this.notifyObservers();
1✔
133
        }
134
    }
1✔
135

136
    protected void setId(String id) {
137
        this.properties.put("@id", id);
1✔
138
    }
1✔
139

140
    /**
141
     * removes one property from an entity.
142
     *
143
     * @param key the key of the entity, which will be removed.
144
     */
145
    public void removeProperty(String key) {
146
        this.getProperties().remove(key);
1✔
147
        this.notifyObservers();
1✔
148
    }
1✔
149

150
    /**
151
     * Removes a collection of properties from an entity.
152
     *
153
     * @param keys collection of keys, which will be removed.
154
     */
155
    public void removeProperties(Collection<String> keys) {
156
        this.getProperties().remove(keys);
1✔
157
        this.notifyObservers();
1✔
158
    }
1✔
159

160
    /**
161
     * Adds a property to the entity.
162
     * <p>
163
     * It may override values, if the key already exists.
164
     *
165
     * @param key the key of the property.
166
     * @param value value of the property.
167
     */
168
    public void addProperty(String key, String value) {
169
        if (key != null && value != null) {
1✔
170
            this.properties.put(key, value);
1✔
171
            this.notifyObservers();
1✔
172
        }
173
    }
1✔
174

175
    /**
176
     * Adds a property to the entity.
177
     * <p>
178
     * It may override values, if the key already exists.
179
     *
180
     * @param key the key of the property.
181
     * @param value value of the property.
182
     */
183
    public void addProperty(String key, long value) {
184
        if (key != null) {
×
185
            this.properties.put(key, value);
×
186
            this.notifyObservers();
×
187
        }
188
    }
×
189

190
    /**
191
     * Adds a property to the entity.
192
     * <p>
193
     * It may override values, if the key already exists.
194
     *
195
     * @param key the key of the property.
196
     * @param value value of the property.
197
     */
198
    public void addProperty(String key, double value) {
199
        if (key != null) {
×
200
            this.properties.put(key, value);
×
201
            this.notifyObservers();
×
202
        }
203
    }
×
204

205
    /**
206
     * Adds a generic property to the entity.
207
     * <p>
208
     * It may fail with an error message on stdout, in which case the
209
     * value is not added.
210
     * It may override values, if the key already exists.
211
     * This is the most generic way to add a property. The value is a
212
     * JsonNode that could contain anything possible. It is limited to
213
     * objects allowed to flattened documents, which means any literal,
214
     * an array of literals, or an object with an @id property.
215
     *
216
     * @param key   String key of the property.
217
     * @param value The JsonNode representing the value.
218
     */
219
    public void addProperty(String key, JsonNode value) {
220
        if (addProperty(this.properties, key, value)) {
×
221
            notifyObservers();
×
222
        }
223
    }
×
224

225
    private static boolean addProperty(ObjectNode whereToAdd, String key, JsonNode value) {
226
        boolean validInput = key != null && value != null;
1✔
227
        if (validInput && entityValidation.fieldValidation(value)) {
1✔
228
            whereToAdd.set(key, value);
1✔
229
            return true;
1✔
230
        }
231
        return false;
1✔
232
    }
233

234
    /**
235
     * Add a property that looks like this: "name" : {"@id" : "id"} If the
236
     * name property already exists add a second @id to it.
237
     *
238
     * @param name the "key" of the property.
239
     * @param id the "id" of the property.
240
     */
241
    public void addIdProperty(String name, String id) {
242
        if (id == null || id.isBlank()) { return; }
1✔
243
        mergeIdIntoValue(id, this.properties.get(name))
1✔
244
                .ifPresent(newValue -> {
1✔
245
                    this.properties.set(name, newValue);
1✔
246
                });
1✔
247
        this.linkedTo.add(id);
1✔
248
        this.notifyObservers();
1✔
249
    }
1✔
250

251
    /**
252
     * Merges the given id into the current value,
253
     * using this representation: {"@id" : "id"}.
254
     * <p>
255
     * The current value can be null without errors.
256
     * Only the id will be considered in this case.
257
     * <p>
258
     * If the id is null-ish, it will not be added, similar to a null-ish value.
259
     * If the id is already present, nothing will be done.
260
     * If it is not an array and the id is not present, an array will be applied.
261
     *
262
     * @param id the id to add.
263
     * @param currentValue the current value of the property.
264
     * @return The updated value of the property.
265
     *               Empty if value does not change!
266
     */
267
    protected static Optional<JsonNode> mergeIdIntoValue(String id, JsonNode currentValue) {
268
        if (id == null || id.isBlank()) { return Optional.empty(); }
1✔
269

270
        ObjectMapper jsonBuilder = MyObjectMapper.getMapper();
1✔
271
        ObjectNode newIdObject = jsonBuilder.createObjectNode().put("@id", id);
1✔
272
        if (currentValue == null || currentValue.isNull() || currentValue.isMissingNode()) {
1✔
273
            return Optional.ofNullable(newIdObject);
1✔
274
        }
275

276
        boolean isIdAlready = currentValue.asText().equals(id);
1✔
277
        boolean isIdObjectAlready = currentValue.path("@id").asText().equals(id);
1✔
278
        boolean isArrayWithIdPresent = currentValue.valueStream()
1✔
279
                .anyMatch(node -> node
1✔
280
                        .path("@id")
1✔
281
                        .asText()
1✔
282
                        .equals(id));
1✔
283
        if (isIdAlready || isIdObjectAlready || isArrayWithIdPresent) {
1✔
284
            return Optional.empty();
1✔
285
        }
286

287
        if (currentValue.isArray() && currentValue instanceof ArrayNode currentValueAsArray) {
1✔
288
            currentValueAsArray.add(newIdObject);
1✔
289
            return Optional.of(currentValueAsArray);
1✔
290
        } else {
291
            // property is not an array, so we make it an array
292
            ArrayNode newNodes = jsonBuilder.createArrayNode();
1✔
293
            newNodes.add(currentValue);
1✔
294
            newNodes.add(newIdObject);
1✔
295
            return Optional.of(newNodes);
1✔
296
        }
297
    }
298

299
    /**
300
     * Adds everything from the properties to the property "name" as id.
301
     *
302
     * @param name the key of the property.
303
     * @param properties a collection containing all the id as String.
304
     */
305
    public void addIdListProperties(String name, Collection<String> properties) {
306
        ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
307
        ArrayNode node = objectMapper.createArrayNode();
1✔
308
        if (this.properties.get(name) == null) {
1✔
309
            node = objectMapper.createArrayNode();
1✔
310
        } else {
311
            if (!this.properties.get(name).isArray()) {
×
312
                node.add(this.properties.get(name));
×
313
            }
314
        }
315
        for (String s : properties) {
1✔
316
            this.linkedTo.add(s);
1✔
317
            node.add(objectMapper.createObjectNode().put("@id", s));
1✔
318
        }
1✔
319
        if (node.size() == 1) {
1✔
320
            this.properties.set(name, node.get(0));
1✔
321
        } else {
322
            this.properties.set(name, node);
1✔
323
        }
324
        notifyObservers();
1✔
325
    }
1✔
326

327
    /**
328
     * Adding new type to the property (which may have multiple such ones).
329
     *
330
     * @param type the String representing the type.
331
     */
332
    public void addType(String type) {
333
        if (this.types == null) {
1✔
334
            this.types = new HashSet<>();
1✔
335
        }
336
        this.types.add(type);
1✔
337
        JsonNode node = MyObjectMapper.getMapper().valueToTree(this.types);
1✔
338
        this.properties.set("@type", node);
1✔
339
    }
1✔
340

341
    /**
342
     * Checks if the date matches the ISO 8601 date format.
343
     *
344
     * @param date the date as a string
345
     * @throws IllegalArgumentException if format does not match
346
     */
347
    private static void checkFormatISO8601(String date) throws IllegalArgumentException {
348
        String regex = "^([\\+-]?\\d{4}(?!\\d{2}\\b))((-?)((0[1-9]|1[0-2])(\\3([12]\\d|0[1-9]|3[01]))?|W([0-4]\\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\\d|[12]\\d{2}|3([0-5]\\d|6[1-6])))([T\\s]((([01]\\d|2[0-3])((:?)[0-5]\\d)?|24\\:?00)([\\.,]\\d+(?!:))?)?(\\17[0-5]\\d([\\.,]\\d+)?)?([zZ]|([\\+-])([01]\\d|2[0-3]):?([0-5]\\d)?)?)?)?$";
1✔
349
        Pattern pattern = Pattern.compile(regex);
1✔
350
        Matcher matcher = pattern.matcher(date);
1✔
351
        if (!matcher.matches()) {
1✔
352
            throw new IllegalArgumentException("Date MUST be a string in ISO 8601 format");
×
353
        }
354
    }
1✔
355

356
    /**
357
     * Adds a property with date time format. The property should match the ISO 8601
358
     * date format.
359
     * 
360
     * Same as {@link #addProperty(String, String)} but with internal check.
361
     *
362
     * @param key   key of the property (e.g. datePublished)
363
     * @param value time string in ISO 8601 format
364
     * @throws IllegalArgumentException if format is not ISO 8601
365
     */
366
    public void addDateTimePropertyWithExceptions(String key, String value) throws IllegalArgumentException {
367
        if (value != null) {
1✔
368
            checkFormatISO8601(value);
1✔
369
            this.properties.put(key, value);
1✔
370
            this.notifyObservers();
1✔
371
        }
372
    }
1✔
373

374
    /**
375
     * This a builder inner class that should allow for an easier creating of
376
     * entities.
377
     *
378
     * @param <T> The type of the child builders so that they to can use the
379
     * methods provided here.
380
     */
381
    public abstract static class AbstractEntityBuilder<T extends AbstractEntityBuilder<T>> {
382

383
        private Set<String> types;
384
        protected Set<String> relatedItems;
385
        private ObjectNode properties;
386
        private String id;
387

388
        protected AbstractEntityBuilder() {
1✔
389
            this.properties = MyObjectMapper.getMapper().createObjectNode();
1✔
390
            this.relatedItems = new HashSet<>();
1✔
391
        }
1✔
392

393
        protected String getId() {
394
            return this.id;
1✔
395
        }
396

397
        /**
398
         * Setting the id property of the entity, if the given value is not
399
         * null. If the id is not encoded, the encoding will be done.
400
         * <p>
401
         * <b>NOTE: IDs are not just names!</b> The ID may have effects
402
         * on parts of your crate! For example: If the entity represents a
403
         * file which will be copied into the crate, writers must use the
404
         * ID as filename.
405
         *
406
         * @param id the String representing the id.
407
         * @return the generic builder.
408
         */
409
        public T setId(String id) {
410
            if (id != null && !id.equals(RootDataEntity.ID)) {
1✔
411
                if (IdentifierUtils.isValidUri(id)) {
1✔
412
                    this.id = id;
1✔
413
                } else {
414
                    this.id = IdentifierUtils.encode(id).get();
1✔
415
                }
416
            }
417
            return self();
1✔
418
        }
419

420
        /**
421
         * Adding a type to the builder types.
422
         *
423
         * @param type the type to add.
424
         * @return the generic builder.
425
         */
426
        public T addType(String type) {
427
            if (this.types == null) {
1✔
428
                this.types = new HashSet<>();
1✔
429
            }
430
            this.types.add(type);
1✔
431
            return self();
1✔
432
        }
433

434
        /**
435
         * Adds multiple types in one call.
436
         *
437
         * @param types the types to add.
438
         * @return the generic builder.
439
         */
440
        public T addTypes(Collection<String> types) {
441
            if (this.types == null) {
×
442
                this.types = new HashSet<>();
×
443
            }
444
            this.types.addAll(types);
×
445
            return self();
×
446
        }
447

448
        /**
449
         * Adds a property with date time format. The property should match the ISO 8601
450
         * date format.
451
         * 
452
         * Same as {@link #addProperty(String, String)} but with internal check.
453
         *
454
         * @param key   key of the property (e.g. datePublished)
455
         * @param value time string in ISO 8601 format
456
         * @return this builder
457
         * @throws IllegalArgumentException if format is not ISO 8601
458
         */
459
        public T addDateTimePropertyWithExceptions(String key, String value) throws IllegalArgumentException {
460
            if (value != null) {
1✔
461
                checkFormatISO8601(value);
1✔
462
                this.properties.put(key, value);
1✔
463
            }
464
            return self();
1✔
465
        }
466

467
        /**
468
         * Adding a property to the builder.
469
         *
470
         * @param key the key of the property in a string.
471
         * @param value the JsonNode value of te property.
472
         * @return the generic builder.
473
         */
474
        public T addProperty(String key, JsonNode value) {
475
            if (AbstractEntity.addProperty(this.properties, key, value)) {
1✔
476
                this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromProperty(value));
1✔
477
            }
478
            return self();
1✔
479
        }
480

481
        /**
482
         * Adding a property to the builder.
483
         *
484
         * @param key the key of the property as a string.
485
         * @param value the value of the property as a string.
486
         * @return the generic builder.
487
         */
488
        public T addProperty(String key, String value) {
489
            this.properties.put(key, value);
1✔
490
            return self();
1✔
491
        }
492

493
        public T addProperty(String key, int value) {
494
            this.properties.put(key, value);
×
495
            return self();
×
496
        }
497

498
        public T addProperty(String key, double value) {
499
            this.properties.put(key, value);
×
500
            return self();
×
501
        }
502

503
        public T addProperty(String key, boolean value) {
504
            this.properties.put(key, value);
1✔
505
            return self();
1✔
506
        }
507

508
        /**
509
         * ID properties are often used when referencing other entities within
510
         * the ROCrate. This method adds automatically such one.
511
         * 
512
         * Instead of {@code "name": "id" }
513
         * this will add {@code "name" : {"@id": "id"} }
514
         * 
515
         * Does nothing if name or id are null.
516
         *
517
         * @param name the name of the ID property.
518
         * @param id the ID.
519
         * @return the generic builder
520
         */
521
        public T addIdProperty(String name, String id) {
522
            AbstractEntity.mergeIdIntoValue(id, this.properties.get(name))
1✔
523
                    .ifPresent(newValue -> {
1✔
524
                        this.properties.set(name, newValue);
1✔
525
                        this.relatedItems.add(id);
1✔
526
                    });
1✔
527
            return self();
1✔
528
        }
529

530
        /**
531
         * This is another way of adding the ID property, this time the whole
532
         * other Entity is provided.
533
         *
534
         * @param name the name of the property.
535
         * @param entity the other entity that is referenced.
536
         * @return the generic builder.
537
         */
538
        public T addIdProperty(String name, AbstractEntity entity) {
539
            if (entity != null) {
1✔
540
                return addIdProperty(name, entity.getId());
1✔
541
            }
542
            return self();
1✔
543
        }
544

545
        /**
546
         * This adds multiple id entities to a single key.
547
         *
548
         * @param name the name of the property.
549
         * @param entities the Collection containing the multiple entities.
550
         * @return the generic builder.
551
         */
552
        public T addIdFromCollectionOfEntities(String name, Collection<AbstractEntity> entities) {
553
            if (entities != null) {
1✔
554
                for (var e : entities) {
1✔
555
                    addIdProperty(name, e);
1✔
556
                }
1✔
557
            }
558
            return self();
1✔
559
        }
560

561
        /**
562
         * Deprecated. Equivalent to {@link #setAllIfValid(ObjectNode)}.
563
         *
564
         * @param properties the Json representing all the properties.
565
         * @return the generic builder, either including all given properties
566
         *          * or unchanged.
567
         *
568
         * @deprecated To enforce the user know what this method does,
569
         *   we want the user to use one of the more explicitly named
570
         *   methods {@link #setAllIfValid(ObjectNode)} or
571
         *   {@link #setAllIfValid(ObjectNode)}.
572
         * @see #setAllIfValid(ObjectNode)
573
         */
574
        @Deprecated(since = "2.1.0", forRemoval = true)
575
        public T setAll(ObjectNode properties) {
NEW
576
            return setAllIfValid(properties);
×
577
        }
578

579
        /**
580
         * This sets everything from a json object to the property,
581
         * <b>if the result is valid</b>. Otherwise, it will do <b>nothing</b>.
582
         * <p>
583
         * Valid means here that the json object needs to be flat as specified
584
         * in the RO-Crate specification. In principle, this means that
585
         * primitives and objects referencing an ID are allowed,
586
         * as well as arrays of these.
587
         *
588
         * @param properties the Json representing all the properties.
589
         * @return the generic builder, either including all given properties
590
         * or unchanged.
591
         */
592
        public T setAllIfValid(ObjectNode properties) {
593
            if (AbstractEntity.entityValidation.entityValidation(properties)) {
1✔
594
                this.properties = properties;
1✔
595
                this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromJsonNode(properties));
1✔
596
            }
597
            return self();
1✔
598
        }
599

600
        /**
601
         * This sets everything from a json object to the property. Can be
602
         * useful when the entity is already available somewhere.
603
         * <p>
604
         * Errors on validation are printed, but everything will be added.
605
         * For more about validation, see {@link #setAllIfValid(ObjectNode)}.
606
         *
607
         * @param properties the Json representing all the properties.
608
         * @return the generic builder with all properties added.
609
         */
610
        public T setAllUnsafe(ObjectNode properties) {
611
            // This will currently only print errors.
612
            AbstractEntity.entityValidation.entityValidation(properties);
1✔
613
            this.properties = properties;
1✔
614
            this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromJsonNode(properties));
1✔
615
            return self();
1✔
616
        }
617

618
        public abstract T self();
619

620
        public abstract AbstractEntity build();
621
    }
622

623
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc