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

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

02 May 2025 09:57AM UTC coverage: 89.412% (+0.06%) from 89.357%
#409

Pull #256

github

web-flow
Merge 4e140198e into f1a1a72a0
Pull Request #256: Make specification examples (from readme) executable

32 of 32 new or added lines in 2 files covered. (100.0%)

22 existing lines in 3 files now uncovered.

1900 of 2125 relevant lines covered (89.41%)

0.89 hits per line

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

86.9
/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 (name == null) { return; }
1✔
243
        mergeIdIntoValue(id, this.properties.get(name))
1✔
244
                .ifPresent(newValue -> {
1✔
245
                    this.linkedTo.add(id);
1✔
246
                    this.properties.set(name, newValue);
1✔
247
                    this.notifyObservers();
1✔
248
                });
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
    private 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()) {
×
UNCOV
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✔
UNCOV
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
         *
401
         * @param id the String representing the id.
402
         * @return the generic builder.
403
         */
404
        public T setId(String id) {
405
            if (id != null && !id.equals(RootDataEntity.ID)) {
1✔
406
                if (IdentifierUtils.isValidUri(id)) {
1✔
407
                    this.id = id;
1✔
408
                } else {
409
                    this.id = IdentifierUtils.encode(id).get();
1✔
410
                }
411
            }
412
            return self();
1✔
413
        }
414

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

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

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

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

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

488
        public T addProperty(String key, int value) {
UNCOV
489
            this.properties.put(key, value);
×
UNCOV
490
            return self();
×
491
        }
492

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

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

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

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

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

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

571
        public abstract T self();
572

573
        public abstract AbstractEntity build();
574
    }
575

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