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

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

06 Jun 2025 01:09PM UTC coverage: 90.804% (+0.09%) from 90.716%
#480

push

github

web-flow
Merge pull request #264 from kit-data-manager/development

Next Version (2.1.0 | 2.1.0-rc3)

90 of 93 new or added lines in 9 files covered. (96.77%)

3 existing lines in 1 file now uncovered.

2044 of 2251 relevant lines covered (90.8%)

0.91 hits per line

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

94.17
/src/main/java/edu/kit/datamanager/ro_crate/context/RoCrateMetadataContext.java
1
package edu.kit.datamanager.ro_crate.context;
2

3
import com.fasterxml.jackson.core.type.TypeReference;
4
import com.fasterxml.jackson.databind.JsonNode;
5
import com.fasterxml.jackson.databind.ObjectMapper;
6
import com.fasterxml.jackson.databind.node.ArrayNode;
7
import com.fasterxml.jackson.databind.node.ObjectNode;
8

9
import edu.kit.datamanager.ro_crate.entities.AbstractEntity;
10
import edu.kit.datamanager.ro_crate.objectmapper.MyObjectMapper;
11

12
import java.io.IOException;
13
import java.util.*;
14
import java.util.function.Consumer;
15
import java.util.function.Function;
16

17
import edu.kit.datamanager.ro_crate.special.IdentifierUtils;
18
import org.apache.http.client.methods.CloseableHttpResponse;
19
import org.apache.http.client.methods.HttpGet;
20
import org.apache.http.impl.client.CloseableHttpClient;
21
import org.apache.http.impl.client.HttpClients;
22

23
/**
24
 * The class representing the crate json-ld context.
25
 *
26
 * @author Nikola Tzotchev on 6.2.2022 г.
27
 * @version 1
28
 */
29
public class RoCrateMetadataContext implements CrateMetadataContext {
30

31
  public static final String DEFAULT_CONTEXT = "https://w3id.org/ro/crate/1.1/context";
32
  protected static final String DEFAULT_CONTEXT_LOCATION = "default_context/version1.1.json";
33
  protected static JsonNode defaultContext = null;
1✔
34

35
  protected final Set<String> urls = new HashSet<>();
1✔
36
  protected final HashMap<String, String> contextMap = new HashMap<>();
1✔
37
  // we need to keep the ones that are no coming from url
38
  // for the final representation
39
  protected final HashMap<String, String> other = new HashMap<>();
1✔
40

41
  /**
42
   * Default constructor for the creation of the v1.1 default context.
43
   */
44
  public RoCrateMetadataContext() {
1✔
45
    this.addToContextFromUrl(DEFAULT_CONTEXT);
1✔
46
  }
1✔
47

48
  /**
49
   * Constructor for creating the context from a list of url.
50
   * <p>
51
   * Note: Does NOT contain the default context if not explicitly given!
52
   *
53
   * @param urls the url list with different context.
54
   */
55
  public RoCrateMetadataContext(Collection<String> urls) {
1✔
56
    urls.forEach(this::addToContextFromUrl);
1✔
57
  }
1✔
58

59
  /**
60
   * Constructor for creating the context from a json object.
61
   * <p>
62
   * Note: Does NOT contain the default context if not explicitly given!
63
   *
64
   * @param context the Json object of the context.
65
   */
66
  public RoCrateMetadataContext(JsonNode context) {
1✔
67

68
    Consumer<JsonNode> addPairs = x -> {
1✔
69
      var iterate = x.fields();
1✔
70
      while (iterate.hasNext()) {
1✔
71
        var next = iterate.next();
1✔
72
        this.other.put(next.getKey(), next.getValue().asText());
1✔
73
        this.contextMap.put(next.getKey(), next.getValue().asText());
1✔
74
      }
1✔
75
    };
1✔
76
    if (context.isArray()) {
1✔
77
      for (JsonNode jsonNode : context) {
1✔
78
        if (jsonNode.isTextual()) {
1✔
79
          this.addToContextFromUrl(jsonNode.asText());
1✔
80
        } else if (jsonNode.isObject()) {
1✔
81
          addPairs.accept(jsonNode);
1✔
82
        }
83
      }
1✔
84
    } else if (context.isObject()) {
1✔
85
      addPairs.accept(context);
1✔
86
    } else {
87
      this.addToContextFromUrl(context.asText());
1✔
88
    }
89
  }
1✔
90

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
   */
113
  @Override
114
  public ObjectNode getContextJsonEntity() {
115
    ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
116
    ArrayNode array = objectMapper.createArrayNode();
1✔
117
    ObjectNode jsonNode = objectMapper.createObjectNode();
1✔
118
    ObjectNode finalNode = objectMapper.createObjectNode();
1✔
119
    for (String e : urls) {
1✔
120
      array.add(e);
1✔
121
    }
1✔
122
    for (Map.Entry<String, String> s : other.entrySet()) {
1✔
123
      jsonNode.put(s.getKey(), s.getValue());
1✔
124
    }
1✔
125
    if (!jsonNode.isEmpty()) {
1✔
126
      array.add(jsonNode);
1✔
127
    }
128
    if (array.size() == 1) {
1✔
129
      finalNode.set("@context", array.get(0));
1✔
130
      return finalNode;
1✔
131
    }
132
    finalNode.set("@context", array);
1✔
133
    return finalNode;
1✔
134
  }
135

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
   */
151
  @Override
152
  public boolean checkEntity(AbstractEntity entity) {
153
    ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
154
    ObjectNode node = entity.getProperties().deepCopy();
1✔
155
    node.remove("@id");
1✔
156
    node.remove("@type");
1✔
157

158
    Set<String> types = objectMapper.convertValue(
1✔
159
            entity.getProperties().path("@type"),
1✔
160
            new TypeReference<>() {}
1✔
161
    );
162

163
    final Function<String, Boolean> isFail = checkMeStr -> this.contextMap.get(checkMeStr) == null
1✔
164
            && this.contextMap.keySet().stream()
1✔
165
            .noneMatch(key -> checkMeStr.startsWith(key + ":"));
1✔
166

167
    // check if the items in the array of types are present in the context
168
    for (String s : types) {
1✔
169
      // special cases:
170
      if (s.equals("@id")) {
1✔
171
        // @id will refer to the value of the id of the node
172
        // so we need to extract this value
173
        s = entity.getId();
1✔
174
      }
175
      if (s.equals("@json")) {
1✔
176
        // A linked data builtin type, which is fine.
177
        continue;
1✔
178
      }
179
      if (IdentifierUtils.isUrl(s)) {
1✔
180
        // full URLs are considered fine
181
        continue;
1✔
182
      }
183

184
      if (isFail.apply(s)) {
1✔
185
        System.err.println("type " + s + " is missing from the context!");
1✔
186
        return false;
1✔
187
      }
188
    }
1✔
189

190
    // check if the fields of the entity are present in the context
191
    for (var names = node.fieldNames(); names.hasNext();) {
1✔
192
      String s = names.next();
1✔
193
      if (IdentifierUtils.isUrl(s)) {
1✔
194
        // full URLs are considered fine
195
        continue;
1✔
196
      }
197
      if (isFail.apply(s)) {
1✔
198
        System.err.println("attribute name " + s + " is missing from context;");
1✔
199
        return false;
1✔
200
      }
201
    }
1✔
202
    return true;
1✔
203
  }
204

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
   */
212
  @Override
213
  public void addToContextFromUrl(String url) {
214
    this.urls.add(url);
1✔
215

216
    ObjectMapper objectMapper = MyObjectMapper.getMapper();
1✔
217

218
    JsonNode jsonNode = null;
1✔
219
    if (url.equals(DEFAULT_CONTEXT)) {
1✔
220
      if (defaultContext != null) {
1✔
221
        jsonNode = defaultContext;
×
222
      } else {
223
        try {
224
          jsonNode = objectMapper.readTree(
1✔
225
              getClass().getClassLoader().getResource(DEFAULT_CONTEXT_LOCATION));
1✔
226
        } catch (IOException e) {
×
227
          e.printStackTrace();
×
228
        }
1✔
229
      }
230
    }
231
    if (jsonNode == null) {
1✔
232
      HttpGet httpGet = new HttpGet(url);
1✔
233
      CloseableHttpResponse response;
234
      try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
1✔
235
        response = httpclient.execute(httpGet);
1✔
UNCOV
236
        jsonNode = objectMapper.readValue(response.getEntity().getContent(),
×
237
            JsonNode.class);
238
      } catch (IOException e) {
1✔
239
        System.err.printf("Cannot get context from url %s%n", url);
1✔
240
        return;
1✔
UNCOV
241
      }
×
UNCOV
242
      if (url.equals(DEFAULT_CONTEXT)) {
×
243
        defaultContext = jsonNode;
×
244
      }
245
    }
246
    this.contextMap.putAll(objectMapper.convertValue(jsonNode.get("@context"),
1✔
247
        new TypeReference<>() {
1✔
248
        }));
249
  }
1✔
250

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
     */
257
  @Override
258
  public void addToContext(String key, String value) {
259
    this.contextMap.put(key, value);
1✔
260
    this.other.put(key, value);
1✔
261
  }
1✔
262

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
   */
267
  @Override
268
  public String getValueOf(String key) {
269
    return Optional.ofNullable(this.contextMap.get(key))
1✔
270
            .orElseGet(() -> this.other.get(key));
1✔
271
  }
272

273
  /**
274
   * @return the set of all keys in the context.
275
   */
276
  @Override
277
  public Set<String> getKeys() {
278
    List<String> merged = new ArrayList<>();
1✔
279
    merged.addAll(this.contextMap.keySet());
1✔
280
    merged.addAll(this.other.keySet());
1✔
281
    return Set.copyOf(merged);
1✔
282
  }
283

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
   */
289
  @Override
290
  public Map<String, String> getPairs() {
291
    Map<String, String> merged = new HashMap<>();
1✔
292
    merged.putAll(this.contextMap);
1✔
293
    merged.putAll(this.other);
1✔
294
    return Map.copyOf(merged);
1✔
295
  }
296

297
  /**
298
   * @param key the key to delete from the context.
299
   */
300
  @Override
301
  public void deleteValuePairFromContext(String key) {
302
    this.contextMap.remove(key);
1✔
303
    this.other.remove(key);
1✔
304
  }
1✔
305

306
  /**
307
   * @param url the URL to delete from the context.
308
   */
309
  @Override
310
  public void deleteUrlFromContext(String url) {
311
    this.urls.remove(url);
1✔
312
  }
1✔
313

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