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

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

10 Nov 2025 11:30AM UTC coverage: 90.126% (-0.7%) from 90.804%
#572

Pull #279

github

web-flow
fix: only add entity types which are not null AND not empty

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Pull Request #279: Next Version (2.1.0-rc4)

231 of 248 new or added lines in 8 files covered. (93.15%)

31 existing lines in 2 files now uncovered.

2209 of 2451 relevant lines covered (90.13%)

0.9 hits per line

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

94.51
/src/main/java/edu/kit/datamanager/ro_crate/hierarchy/HierarchyRecognition.java
1
package edu.kit.datamanager.ro_crate.hierarchy;
2

3
import edu.kit.datamanager.ro_crate.Crate;
4
import edu.kit.datamanager.ro_crate.entities.data.DataEntity;
5
import edu.kit.datamanager.ro_crate.entities.data.DataSetEntity;
6
import edu.kit.datamanager.ro_crate.util.FileSystemUtil;
7

8
import java.util.HashMap;
9
import java.util.HashSet;
10
import java.util.Map;
11
import java.util.Set;
12

13
public class HierarchyRecognition {
14
    protected final Crate crate;
15
    protected final HierarchyRecognitionConfig config;
16

17
    public HierarchyRecognition(Crate crate, HierarchyRecognitionConfig config) {
1✔
18
        this.crate = crate;
1✔
19
        this.config = config;
1✔
20
    }
1✔
21

22
    public HierarchyRecognitionResult buildHierarchy() {
23
        HierarchyRecognitionResult result = new HierarchyRecognitionResult();
1✔
24

25
        try {
26
            // Get all data entities to process
27
            Set<DataEntity> allEntities = this.crate.getAllDataEntities();
1✔
28
            allEntities.add(crate.getRootDataEntity());
1✔
29

30
            // Filter entities with file path IDs (not URLs, DOIs, etc.)
31
            Map<String, DataEntity> pathEntities = new HashMap<>();
1✔
32
            for (DataEntity entity : allEntities) {
1✔
33
                String id = entity.getId();
1✔
34
                if (FileSystemUtil.isFilePath(id)) {
1✔
35
                    pathEntities.put(id, entity);
1✔
36
                } else {
NEW
37
                    result.addSkippedEntity(entity);
×
38
                }
39
            }
1✔
40

41

42
            // Validate hierarchy before making changes
43
            if (!HierarchyRecognition.validateHierarchy(pathEntities, result)) {
1✔
44
                return result;
1✔
45
            }
46

47
            // Create missing intermediate entities if configured
48
            if (config.createMissingIntermediateEntities()) {
1✔
49
                this.createMissingIntermediateEntities(pathEntities, result);
1✔
50
            }
51

52
            // Clear existing relationships if configured
53
            if (config.removeExistingConnections()) {
1✔
54
                this.clearExistingRelationships(pathEntities);
1✔
55
            }
56

57
            // Build hierarchy relationships
58
            this.buildHierarchyRelationships(pathEntities, config, result);
1✔
59

60
            return result;
1✔
NEW
61
        } catch (Exception e) {
×
NEW
62
            result.addError(
×
63
                    "Unexpected error during hierarchy recognition: " +
NEW
64
                            e.getMessage()
×
65
            );
NEW
66
            return result;
×
67
        }
68
    }
69

70
    /**
71
     * Validates that the hierarchy is consistent (no files containing other files/folders).
72
     *
73
     * @param pathEntities map of path IDs to DataEntities
74
     * @param result builder to collect errors
75
     * @return true if valid, false if invalid hierarchy detected
76
     */
77
    protected static boolean validateHierarchy(
78
            Map<String, DataEntity> pathEntities,
79
            HierarchyRecognitionResult result
80
    ) {
81
        for (Map.Entry<String, DataEntity> entry : pathEntities.entrySet()) {
1✔
82
            String childId = entry.getKey();
1✔
83
            String parentPath = FileSystemUtil.getParentPath(childId);
1✔
84
            if (parentPath == null || parentPath.equals("./")) {
1✔
85
                continue;
1✔
86
            }
87

88
            // Check both with and without trailing slash since files don't have slash but folders do
89
            DataEntity parentEntity = pathEntities.get(parentPath);
1✔
90
            if (parentEntity == null) {
1✔
91
                parentEntity = pathEntities.get(parentPath + "/");
1✔
92
            }
93

94
            if (parentEntity == null) {
1✔
95
                continue;
1✔
96
            }
97

98
            // Check for invalid hierarchy: file cannot contain another file/folder
99
            if (parentEntity.getTypes().contains("File")) {
1✔
100
                result.addError(
1✔
101
                        "Invalid hierarchy: file '" +
102
                                parentEntity.getId() +
1✔
103
                                "' cannot contain '" +
104
                                childId +
105
                                "'"
106
                );
107
                return false;
1✔
108
            }
109
        }
1✔
110
        return true;
1✔
111
    }
112

113
    /**
114
     * Creates missing intermediate DataSetEntity instances for folder paths.
115
     *
116
     * @param pathEntities map of path IDs to DataEntities
117
     * @param result builder to collect created entities
118
     */
119
    protected void createMissingIntermediateEntities(
120
            Map<String, DataEntity> pathEntities,
121
            HierarchyRecognitionResult result
122
    ) {
123
        Set<String> missingPaths = new HashSet<>();
1✔
124

125
        // Find all missing intermediate paths
126
        for (String path : pathEntities.keySet()) {
1✔
127
            String parentPath = FileSystemUtil.getParentPath(path);
1✔
128
            while (parentPath != null && !parentPath.equals("./")) {
1✔
129
                String folderPath = parentPath + "/";
1✔
130
                final boolean containsParent = pathEntities.containsKey(parentPath);
1✔
131
                final boolean containsFolder = pathEntities.containsKey(folderPath);
1✔
132
                if (!containsParent && !containsFolder) {
1✔
133
                    missingPaths.add(folderPath);
1✔
134
                }
135
                parentPath = FileSystemUtil.getParentPath(parentPath);
1✔
136
            }
1✔
137
        }
1✔
138

139
        // Create missing DataSetEntity instances
140
        for (String missingPath : missingPaths) {
1✔
141
            DataSetEntity newEntity = new DataSetEntity.DataSetBuilder()
1✔
142
                    .setId(missingPath)
1✔
143
                    .addProperty("name", "Auto-generated folder: " + missingPath)
1✔
144
                    .build();
1✔
145

146
            this.crate.addDataEntity(newEntity);
1✔
147
            pathEntities.put(missingPath, newEntity);
1✔
148
            result.addCreatedEntity(newEntity);
1✔
149
        }
1✔
150
    }
1✔
151

152
    protected void buildHierarchyRelationships(
153
            Map<String, DataEntity> pathEntities,
154
            HierarchyRecognitionConfig config,
155
            HierarchyRecognitionResult result
156
    ) {
157
        for (Map.Entry<String, DataEntity> entry : pathEntities.entrySet()) {
1✔
158
            String childId = entry.getKey();
1✔
159
            DataEntity childEntity = entry.getValue();
1✔
160
            String parentPath = FileSystemUtil.getParentPath(childId);
1✔
161
            if (parentPath == null) {
1✔
162
                continue;
1✔
163
            }
164

165
            // Check both with and without trailing slash since files don't have slash but folders do
166
            DataEntity parentEntity = pathEntities.get(parentPath);
1✔
167
            String actualParentId = parentPath;
1✔
168

169
            if (parentEntity == null) {
1✔
170
                parentEntity = pathEntities.get(parentPath + "/");
1✔
171
                actualParentId = parentPath + "/";
1✔
172
            }
173

174
            if (parentEntity == null) {
1✔
175
                continue;
1✔
176
            }
177

178
            // Add hasPart relationship
179
            if (parentEntity instanceof DataSetEntity) {
1✔
180
                ((DataSetEntity) parentEntity).addToHasPart(childId);
1✔
181
                result.addProcessedRelationship(
1✔
182
                        actualParentId,
183
                        childId
184
                );
185
            }
186

187
            // Add isPartOf relationship if configured
188
            if (config.createInverseRelationships()) {
1✔
189
                childEntity.addProperty("isPartOf", actualParentId);
1✔
190
            }
191

192
            // Remove from root if it has a parent that is not root
193
            if (!parentPath.equals("./")) {
1✔
194
                this.crate.getRootDataEntity().removeFromHasPart(childId);
1✔
195
            }
196
        }
1✔
197
    }
1✔
198

199
    protected void clearExistingRelationships(
200
            Map<String, DataEntity> pathEntities
201
    ) {
202
        for (DataEntity entity : pathEntities.values()) {
1✔
203
            if (entity instanceof DataSetEntity) {
1✔
204
                ((DataSetEntity) entity).hasPart.clear();
1✔
205
            }
206
        }
1✔
207
    }
1✔
208
}
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