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

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

05 May 2025 08:11AM UTC coverage: 90.169% (+0.8%) from 89.357%
#419

Pull #255

github

web-flow
Merge pull request #256 from kit-data-manager/make-spec-examples-executable

Make specification examples (from readme) executable
Pull Request #255: Next Version (v2.1.0-rc2 | v2.1.0)

63 of 70 new or added lines in 16 files covered. (90.0%)

14 existing lines in 5 files now uncovered.

1917 of 2126 relevant lines covered (90.17%)

0.9 hits per line

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

86.83
/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
        mergeIdIntoValue(id, this.properties.get(name))
1✔
243
                .ifPresent(newValue -> {
1✔
244
                    this.linkedTo.add(id);
1✔
245
                    this.properties.set(name, newValue);
1✔
246
                    this.notifyObservers();
1✔
247
                });
1✔
248
    }
1✔
249

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

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

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

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

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

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

340
    /**
341
     * Checks if the date matches the ISO 8601 date format.
342
     *
343
     * @param date the date as a string
344
     * @throws IllegalArgumentException if format does not match
345
     */
346
    private static void checkFormatISO8601(String date) throws IllegalArgumentException {
347
        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✔
348
        Pattern pattern = Pattern.compile(regex);
1✔
349
        Matcher matcher = pattern.matcher(date);
1✔
350
        if (!matcher.matches()) {
1✔
UNCOV
351
            throw new IllegalArgumentException("Date MUST be a string in ISO 8601 format");
×
352
        }
353
    }
1✔
354

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

560
        /**
561
         * This sets everything from a json object to the property. Can be
562
         * useful when the entity is already available somewhere.
563
         *
564
         * @param properties the Json representing all the properties.
565
         * @return the generic builder.
566
         */
567
        public T setAll(ObjectNode properties) {
568
            if (AbstractEntity.entityValidation.entityValidation(properties)) {
1✔
569
                this.properties = properties;
1✔
570
                this.relatedItems.addAll(JsonUtilFunctions.getIdPropertiesFromJsonNode(properties));
1✔
571
            }
572
            return self();
1✔
573
        }
574

575
        public abstract T self();
576

577
        public abstract AbstractEntity build();
578
    }
579

580
}
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

© 2026 Coveralls, Inc