diff --git a/gradle.properties b/gradle.properties index fcc31adc..1808daac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.1.0-rc2 +version=2.1.0-rc3 action.custom-1=install action.custom-1.args=--configure-on-demand -w -x check clean publishToMavenLocal action.custom-2=jacoco diff --git a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java index 356b0159..32c22dec 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/RoCrate.java @@ -375,12 +375,14 @@ public RoCrateBuilder addDescription(String description) { * @return returns the builder for further usage. */ public RoCrateBuilder addDataEntity(DataEntity dataEntity) { + this.metadataContext.checkEntity(dataEntity); this.payload.addDataEntity(dataEntity); this.rootDataEntity.addToHasPart(dataEntity.getId()); return this; } public RoCrateBuilder addContextualEntity(ContextualEntity contextualEntity) { + this.metadataContext.checkEntity(contextualEntity); this.payload.addContextualEntity(contextualEntity); return this; } diff --git a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java index cce6a7e9..d76cf86b 100644 --- a/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java +++ b/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; import edu.kit.datamanager.ro_crate.special.IdentifierUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -38,7 +39,7 @@ public class RoCrateMetadataContext implements CrateMetadataContext { protected final HashMap other = new HashMap<>(); /** - * Default constructor for the creation of the default context. + * Default constructor for the creation of the v1.1 default context. */ public RoCrateMetadataContext() { this.addToContextFromUrl(DEFAULT_CONTEXT); @@ -46,6 +47,8 @@ public RoCrateMetadataContext() { /** * Constructor for creating the context from a list of url. + *

+ * Note: Does NOT contain the default context if not explicitly given! * * @param urls the url list with different context. */ @@ -55,6 +58,8 @@ public RoCrateMetadataContext(Collection urls) { /** * Constructor for creating the context from a json object. + *

+ * Note: Does NOT contain the default context if not explicitly given! * * @param context the Json object of the context. */ @@ -83,6 +88,28 @@ public RoCrateMetadataContext(JsonNode context) { } } + /** + * Converts the context into a JSON-LD representation. + *

+ * The resulting JSON structure depends on the content: + * - If there's only one URL and no key-value pairs: {"@context": "url"} + * - If there are multiple URLs and/or key-value pairs: {"@context": ["url1", "url2", {"key1": "value1", "key2": "value2"}]} + *

+ * Example output: + *

+   * {
+   *   "@context": [
+   *     "https://w3id.org/ro/crate/1.1/context",
+   *     {
+   *       "schema": "http://schema.org/",
+   *       "rdfs": "http://www.w3.org/2000/01/rdf-schema#"
+   *     }
+   *   ]
+   * }
+   * 
+ * + * @return an ObjectNode containing the JSON-LD context representation + */ @Override public ObjectNode getContextJsonEntity() { ObjectMapper objectMapper = MyObjectMapper.getMapper(); @@ -106,6 +133,21 @@ public ObjectNode getContextJsonEntity() { return finalNode; } + /** + * Checks if the given entity is valid according to the context. + *

+ * - full URLs in the @type and field names are considered valid without further checks. + * - The "@id" value is treated as a special case, where it refers to the entity's ID. + * - The "@json" type is a linked data built-in type and is always considered valid. + * - If a type or field name is not found in the context, it will print an error message and return false. + * - This method checks both the types in the @type array and the field names in the entity's properties. + * - Prefixes in the context are considered valid if they match the context keys. + * - Suffixes after a valid prefix are considered valid in any case. This is not perfect, + * but it would be hard to handle correctly. + * + * @param entity the entity to check + * @return true if the entity is valid, false otherwise + */ @Override public boolean checkEntity(AbstractEntity entity) { ObjectMapper objectMapper = MyObjectMapper.getMapper(); @@ -117,6 +159,11 @@ public boolean checkEntity(AbstractEntity entity) { entity.getProperties().path("@type"), new TypeReference<>() {} ); + + final Function isFail = checkMeStr -> this.contextMap.get(checkMeStr) == null + && this.contextMap.keySet().stream() + .noneMatch(key -> checkMeStr.startsWith(key + ":")); + // check if the items in the array of types are present in the context for (String s : types) { // special cases: @@ -134,7 +181,7 @@ public boolean checkEntity(AbstractEntity entity) { continue; } - if (this.contextMap.get(s) == null) { + if (isFail.apply(s)) { System.err.println("type " + s + " is missing from the context!"); return false; } @@ -147,7 +194,7 @@ public boolean checkEntity(AbstractEntity entity) { // full URLs are considered fine continue; } - if (this.contextMap.get(s) == null) { + if (isFail.apply(s)) { System.err.println("attribute name " + s + " is missing from context;"); return false; } @@ -155,6 +202,13 @@ public boolean checkEntity(AbstractEntity entity) { return true; } + /** + * Adds a URL to the context. + *

+ * It will try to fetch the context from the URL. + * + * @param url the URL to add + */ @Override public void addToContextFromUrl(String url) { this.urls.add(url); @@ -194,18 +248,31 @@ public void addToContextFromUrl(String url) { })); } + /** + * Adds a key-value pair to the context. + * + * @param key the key to add. It may be a prefix or a term. + * @param value the value to add + */ @Override public void addToContext(String key, String value) { this.contextMap.put(key, value); this.other.put(key, value); } + /** + * @param key the key for the value to retrieve. + * @return the value of the key if it exists in the context, null otherwise. + */ @Override public String getValueOf(String key) { return Optional.ofNullable(this.contextMap.get(key)) .orElseGet(() -> this.other.get(key)); } + /** + * @return the set of all keys in the context. + */ @Override public Set getKeys() { List merged = new ArrayList<>(); @@ -214,6 +281,11 @@ public Set getKeys() { return Set.copyOf(merged); } + /** + * @return a map of all key-value pairs in the context. Note that some pairs may come + * from URLs or a pair may not be available as a context was not successfully resolved + * from a URL. + */ @Override public Map getPairs() { Map merged = new HashMap<>(); @@ -222,13 +294,18 @@ public Map getPairs() { return Map.copyOf(merged); } - + /** + * @param key the key to delete from the context. + */ @Override public void deleteValuePairFromContext(String key) { this.contextMap.remove(key); this.other.remove(key); } + /** + * @param url the URL to delete from the context. + */ @Override public void deleteUrlFromContext(String url) { this.urls.remove(url); diff --git a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java similarity index 88% rename from src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java rename to src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java index 4995f272..1fb7b574 100644 --- a/src/test/java/edu/kit/datamanager/ro_crate/context/ContextTest.java +++ b/src/test/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContextTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import edu.kit.datamanager.ro_crate.HelpFunctions; +import edu.kit.datamanager.ro_crate.RoCrate; import edu.kit.datamanager.ro_crate.entities.AbstractEntity; import edu.kit.datamanager.ro_crate.entities.contextual.ContextualEntity; import edu.kit.datamanager.ro_crate.entities.data.DataEntity; @@ -20,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class ContextTest { +public class RoCrateMetadataContextTest { RoCrateMetadataContext context; RoCrateMetadataContext complexContext; @@ -32,7 +33,7 @@ void initContext() throws IOException { final String crateManifestPath = "/crates/extendedContextExample/ro-crate-metadata.json"; ObjectMapper objectMapper = MyObjectMapper.getMapper(); - JsonNode jsonNode = objectMapper.readTree(ContextTest.class.getResourceAsStream(crateManifestPath)); + JsonNode jsonNode = objectMapper.readTree(RoCrateMetadataContextTest.class.getResourceAsStream(crateManifestPath)); this.complexContext = new RoCrateMetadataContext(jsonNode.get("@context")); } @@ -98,8 +99,8 @@ void creationFromPairsJsonTest() { var objectMapper = MyObjectMapper.getMapper(); ObjectNode rawContext = objectMapper.createObjectNode(); - rawContext.put("house", "www.example.con/house"); - rawContext.put("road", "www.example.con/road"); + rawContext.put("house", "www.example.com/house"); + rawContext.put("road", "www.example.com/road"); ObjectNode rawCrate = objectMapper.createObjectNode(); rawCrate.set("@context", rawContext); @@ -107,6 +108,13 @@ void creationFromPairsJsonTest() { assertNotNull(newContext); HelpFunctions.compare(newContext.getContextJsonEntity(), rawCrate, true); + + var entityWithTerms = new ContextualEntity.ContextualEntityBuilder() + .setId("dkfaj") + .addType("house") + .addType("road") + .build(); + assertTrue(newContext.checkEntity(entityWithTerms)); } @Test @@ -278,4 +286,21 @@ void testReadPairs() { // prove immutability assertThrows(UnsupportedOperationException.class, () -> given.put("newKey", "newValue")); } + + @Test + void checkEntity_withDefinedPrefixedType_succeeds() throws IOException { + // assume we read a crate just for demonstration + RoCrate crate = new RoCrate(); + // and we extend the context + RoCrateMetadataContext context = new RoCrateMetadataContext(); + context.addToContext("rdfs", "https://www.w3.org/2000/01/rdf-schema#"); + crate.setMetadataContext(context); + // then we use the new context + DataEntity entity = new DataEntity.DataEntityBuilder() + .addType("rdfs:Property") + .build(); + crate.addDataEntity(entity); + // Then we expect this to work + assertTrue(context.checkEntity(entity)); + } }