Skip to content

Commit bd8aba4

Browse files
authored
Merge pull request #267 from kit-data-manager/add-context-prefix-support
Add context prefix support
2 parents f809820 + 39518cc commit bd8aba4

File tree

4 files changed

+113
-9
lines changed

4 files changed

+113
-9
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=2.1.0-rc2
1+
version=2.1.0-rc3
22
action.custom-1=install
33
action.custom-1.args=--configure-on-demand -w -x check clean publishToMavenLocal
44
action.custom-2=jacoco

src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,12 +375,14 @@ public RoCrateBuilder addDescription(String description) {
375375
* @return returns the builder for further usage.
376376
*/
377377
public RoCrateBuilder addDataEntity(DataEntity dataEntity) {
378+
this.metadataContext.checkEntity(dataEntity);
378379
this.payload.addDataEntity(dataEntity);
379380
this.rootDataEntity.addToHasPart(dataEntity.getId());
380381
return this;
381382
}
382383

383384
public RoCrateBuilder addContextualEntity(ContextualEntity contextualEntity) {
385+
this.metadataContext.checkEntity(contextualEntity);
384386
this.payload.addContextualEntity(contextualEntity);
385387
return this;
386388
}

src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.io.IOException;
1313
import java.util.*;
1414
import java.util.function.Consumer;
15+
import java.util.function.Function;
1516

1617
import edu.kit.datamanager.ro_crate.special.IdentifierUtils;
1718
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -38,14 +39,16 @@ public class RoCrateMetadataContext implements CrateMetadataContext {
3839
protected final HashMap<String, String> other = new HashMap<>();
3940

4041
/**
41-
* Default constructor for the creation of the default context.
42+
* Default constructor for the creation of the v1.1 default context.
4243
*/
4344
public RoCrateMetadataContext() {
4445
this.addToContextFromUrl(DEFAULT_CONTEXT);
4546
}
4647

4748
/**
4849
* Constructor for creating the context from a list of url.
50+
* <p>
51+
* Note: Does NOT contain the default context if not explicitly given!
4952
*
5053
* @param urls the url list with different context.
5154
*/
@@ -55,6 +58,8 @@ public RoCrateMetadataContext(Collection<String> urls) {
5558

5659
/**
5760
* Constructor for creating the context from a json object.
61+
* <p>
62+
* Note: Does NOT contain the default context if not explicitly given!
5863
*
5964
* @param context the Json object of the context.
6065
*/
@@ -83,6 +88,28 @@ public RoCrateMetadataContext(JsonNode context) {
8388
}
8489
}
8590

91+
/**
92+
* Converts the context into a JSON-LD representation.
93+
* <p>
94+
* The resulting JSON structure depends on the content:
95+
* - If there's only one URL and no key-value pairs: {"@context": "url"}
96+
* - If there are multiple URLs and/or key-value pairs: {"@context": ["url1", "url2", {"key1": "value1", "key2": "value2"}]}
97+
* <p>
98+
* Example output:
99+
* <pre>
100+
* {
101+
* "@context": [
102+
* "https://w3id.org/ro/crate/1.1/context",
103+
* {
104+
* "schema": "http://schema.org/",
105+
* "rdfs": "http://www.w3.org/2000/01/rdf-schema#"
106+
* }
107+
* ]
108+
* }
109+
* </pre>
110+
*
111+
* @return an ObjectNode containing the JSON-LD context representation
112+
*/
86113
@Override
87114
public ObjectNode getContextJsonEntity() {
88115
ObjectMapper objectMapper = MyObjectMapper.getMapper();
@@ -106,6 +133,21 @@ public ObjectNode getContextJsonEntity() {
106133
return finalNode;
107134
}
108135

136+
/**
137+
* Checks if the given entity is valid according to the context.
138+
* <p>
139+
* - full URLs in the @type and field names are considered valid without further checks.
140+
* - The "@id" value is treated as a special case, where it refers to the entity's ID.
141+
* - The "@json" type is a linked data built-in type and is always considered valid.
142+
* - If a type or field name is not found in the context, it will print an error message and return false.
143+
* - This method checks both the types in the @type array and the field names in the entity's properties.
144+
* - Prefixes in the context are considered valid if they match the context keys.
145+
* - Suffixes after a valid prefix are considered valid in any case. This is not perfect,
146+
* but it would be hard to handle correctly.
147+
*
148+
* @param entity the entity to check
149+
* @return true if the entity is valid, false otherwise
150+
*/
109151
@Override
110152
public boolean checkEntity(AbstractEntity entity) {
111153
ObjectMapper objectMapper = MyObjectMapper.getMapper();
@@ -117,6 +159,11 @@ public boolean checkEntity(AbstractEntity entity) {
117159
entity.getProperties().path("@type"),
118160
new TypeReference<>() {}
119161
);
162+
163+
final Function<String, Boolean> isFail = checkMeStr -> this.contextMap.get(checkMeStr) == null
164+
&& this.contextMap.keySet().stream()
165+
.noneMatch(key -> checkMeStr.startsWith(key + ":"));
166+
120167
// check if the items in the array of types are present in the context
121168
for (String s : types) {
122169
// special cases:
@@ -134,7 +181,7 @@ public boolean checkEntity(AbstractEntity entity) {
134181
continue;
135182
}
136183

137-
if (this.contextMap.get(s) == null) {
184+
if (isFail.apply(s)) {
138185
System.err.println("type " + s + " is missing from the context!");
139186
return false;
140187
}
@@ -147,14 +194,21 @@ public boolean checkEntity(AbstractEntity entity) {
147194
// full URLs are considered fine
148195
continue;
149196
}
150-
if (this.contextMap.get(s) == null) {
197+
if (isFail.apply(s)) {
151198
System.err.println("attribute name " + s + " is missing from context;");
152199
return false;
153200
}
154201
}
155202
return true;
156203
}
157204

205+
/**
206+
* Adds a URL to the context.
207+
* <p>
208+
* It will try to fetch the context from the URL.
209+
*
210+
* @param url the URL to add
211+
*/
158212
@Override
159213
public void addToContextFromUrl(String url) {
160214
this.urls.add(url);
@@ -194,18 +248,31 @@ public void addToContextFromUrl(String url) {
194248
}));
195249
}
196250

251+
/**
252+
* Adds a key-value pair to the context.
253+
*
254+
* @param key the key to add. It may be a prefix or a term.
255+
* @param value the value to add
256+
*/
197257
@Override
198258
public void addToContext(String key, String value) {
199259
this.contextMap.put(key, value);
200260
this.other.put(key, value);
201261
}
202262

263+
/**
264+
* @param key the key for the value to retrieve.
265+
* @return the value of the key if it exists in the context, null otherwise.
266+
*/
203267
@Override
204268
public String getValueOf(String key) {
205269
return Optional.ofNullable(this.contextMap.get(key))
206270
.orElseGet(() -> this.other.get(key));
207271
}
208272

273+
/**
274+
* @return the set of all keys in the context.
275+
*/
209276
@Override
210277
public Set<String> getKeys() {
211278
List<String> merged = new ArrayList<>();
@@ -214,6 +281,11 @@ public Set<String> getKeys() {
214281
return Set.copyOf(merged);
215282
}
216283

284+
/**
285+
* @return a map of all key-value pairs in the context. Note that some pairs may come
286+
* from URLs or a pair may not be available as a context was not successfully resolved
287+
* from a URL.
288+
*/
217289
@Override
218290
public Map<String, String> getPairs() {
219291
Map<String, String> merged = new HashMap<>();
@@ -222,13 +294,18 @@ public Map<String, String> getPairs() {
222294
return Map.copyOf(merged);
223295
}
224296

225-
297+
/**
298+
* @param key the key to delete from the context.
299+
*/
226300
@Override
227301
public void deleteValuePairFromContext(String key) {
228302
this.contextMap.remove(key);
229303
this.other.remove(key);
230304
}
231305

306+
/**
307+
* @param url the URL to delete from the context.
308+
*/
232309
@Override
233310
public void deleteUrlFromContext(String url) {
234311
this.urls.remove(url);

src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java renamed to src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.fasterxml.jackson.databind.node.ObjectNode;
77

88
import edu.kit.datamanager.ro_crate.HelpFunctions;
9+
import edu.kit.datamanager.ro_crate.RoCrate;
910
import edu.kit.datamanager.ro_crate.entities.AbstractEntity;
1011
import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity;
1112
import edu.kit.datamanager.ro_crate.entities.data.DataEntity;
@@ -20,7 +21,7 @@
2021

2122
import static org.junit.jupiter.api.Assertions.*;
2223

23-
public class ContextTest {
24+
public class RoCrateMetadataContextTest {
2425

2526
RoCrateMetadataContext context;
2627
RoCrateMetadataContext complexContext;
@@ -32,7 +33,7 @@ void initContext() throws IOException {
3233

3334
final String crateManifestPath = "/crates/extendedContextExample/ro-crate-metadata.json";
3435
ObjectMapper objectMapper = MyObjectMapper.getMapper();
35-
JsonNode jsonNode = objectMapper.readTree(ContextTest.class.getResourceAsStream(crateManifestPath));
36+
JsonNode jsonNode = objectMapper.readTree(RoCrateMetadataContextTest.class.getResourceAsStream(crateManifestPath));
3637
this.complexContext = new RoCrateMetadataContext(jsonNode.get("@context"));
3738
}
3839

@@ -98,15 +99,22 @@ void creationFromPairsJsonTest() {
9899
var objectMapper = MyObjectMapper.getMapper();
99100

100101
ObjectNode rawContext = objectMapper.createObjectNode();
101-
rawContext.put("house", "www.example.con/house");
102-
rawContext.put("road", "www.example.con/road");
102+
rawContext.put("house", "www.example.com/house");
103+
rawContext.put("road", "www.example.com/road");
103104

104105
ObjectNode rawCrate = objectMapper.createObjectNode();
105106
rawCrate.set("@context", rawContext);
106107
RoCrateMetadataContext newContext = new RoCrateMetadataContext(rawContext);
107108
assertNotNull(newContext);
108109

109110
HelpFunctions.compare(newContext.getContextJsonEntity(), rawCrate, true);
111+
112+
var entityWithTerms = new ContextualEntity.ContextualEntityBuilder()
113+
.setId("dkfaj")
114+
.addType("house")
115+
.addType("road")
116+
.build();
117+
assertTrue(newContext.checkEntity(entityWithTerms));
110118
}
111119

112120
@Test
@@ -278,4 +286,21 @@ void testReadPairs() {
278286
// prove immutability
279287
assertThrows(UnsupportedOperationException.class, () -> given.put("newKey", "newValue"));
280288
}
289+
290+
@Test
291+
void checkEntity_withDefinedPrefixedType_succeeds() throws IOException {
292+
// assume we read a crate just for demonstration
293+
RoCrate crate = new RoCrate();
294+
// and we extend the context
295+
RoCrateMetadataContext context = new RoCrateMetadataContext();
296+
context.addToContext("rdfs", "https://www.w3.org/2000/01/rdf-schema#");
297+
crate.setMetadataContext(context);
298+
// then we use the new context
299+
DataEntity entity = new DataEntity.DataEntityBuilder()
300+
.addType("rdfs:Property")
301+
.build();
302+
crate.addDataEntity(entity);
303+
// Then we expect this to work
304+
assertTrue(context.checkEntity(entity));
305+
}
281306
}

0 commit comments

Comments
 (0)