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

knowledgepixels / nanodash / 17855398223

19 Sep 2025 10:14AM UTC coverage: 13.734% (+0.05%) from 13.689%
17855398223

push

github

tkuhn
feat: Show start/end dates of spaces

436 of 4030 branches covered (10.82%)

Branch coverage included in aggregate %.

1123 of 7321 relevant lines covered (15.34%)

0.68 hits per line

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

2.76
src/main/java/com/knowledgepixels/nanodash/Space.java
1
package com.knowledgepixels.nanodash;
2

3
import static com.knowledgepixels.nanodash.Utils.vf;
4

5
import java.io.Serializable;
6
import java.time.format.DateTimeParseException;
7
import java.util.ArrayList;
8
import java.util.Calendar;
9
import java.util.Collections;
10
import java.util.HashMap;
11
import java.util.HashSet;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.Set;
15

16
import org.eclipse.rdf4j.model.IRI;
17
import org.eclipse.rdf4j.model.Literal;
18
import org.eclipse.rdf4j.model.Statement;
19
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
20
import org.nanopub.Nanopub;
21
import org.nanopub.extra.services.ApiResponse;
22
import org.nanopub.extra.services.ApiResponseEntry;
23
import org.nanopub.extra.services.QueryRef;
24
import org.nanopub.vocabulary.NTEMPLATE;
25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.github.jsonldjava.shaded.com.google.common.collect.Ordering;
29
import com.knowledgepixels.nanodash.template.Template;
30
import com.knowledgepixels.nanodash.template.TemplateData;
31

32
import jakarta.xml.bind.DatatypeConverter;
33

34
/**
35
 * Class representing a "Space", which can be any kind of collaborative unit, like a project, group, or event.
36
 */
37
public class Space implements Serializable {
38

39
    private static final Logger logger = LoggerFactory.getLogger(Space.class);
3✔
40

41
    /**
42
     * The predicate to assign the admins of the space.
43
     */
44
    public static final IRI HAS_ADMIN = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasAdmin");
4✔
45

46
    /**
47
     * The predicate for pinned templates in the space.
48
     */
49
    public static final IRI HAS_PINNED_TEMPLATE = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedTemplate");
4✔
50

51
    /**
52
     * The predicate for pinned queries in the space.
53
     */
54
    public static final IRI HAS_PINNED_QUERY = vf.createIRI("https://w3id.org/kpxl/gen/terms/hasPinnedQuery");
4✔
55

56
    private static List<Space> spaceList;
57
    private static Map<String, List<Space>> spaceListByType;
58
    private static Map<String,Space> spacesByCoreInfo = new HashMap<>();
4✔
59
    private static Map<String,Space> spacesById;
60
    private static Map<Space,Set<Space>> subspaceMap;
61
    private static Map<Space,Set<Space>> superspaceMap;
62
    private static boolean loaded = false;
3✔
63

64
    public static synchronized void refresh(ApiResponse resp) {
65
        spaceList = new ArrayList<>();
×
66
        spaceListByType = new HashMap<>();
×
67
        Map<String,Space> prevSpacesByCoreInfoPrev = spacesByCoreInfo;
×
68
        spacesByCoreInfo = new HashMap<>();
×
69
        spacesById = new HashMap<>();
×
70
        subspaceMap = new HashMap<>();
×
71
        superspaceMap = new HashMap<>();
×
72
        for (ApiResponseEntry entry : resp.getData()) {
×
73
            Space space = new Space(entry);
×
74
            Space prevSpace = prevSpacesByCoreInfoPrev.get(space.getCoreInfoString());
×
75
            if (prevSpace != null) space = prevSpace;
×
76
            spaceList.add(space);
×
77
            spaceListByType.computeIfAbsent(space.getType(), k -> new ArrayList<>()).add(space);
×
78
            spacesByCoreInfo.put(space.getCoreInfoString(), space);
×
79
            spacesById.put(space.getId(), space);
×
80
        }
×
81
        for (Space space : spaceList) {
×
82
            Space superSpace = space.getIdSuperspace();
×
83
            if (superSpace == null) continue;
×
84
            subspaceMap.computeIfAbsent(superSpace, k -> new HashSet<>()).add(space);
×
85
            superspaceMap.computeIfAbsent(space, k -> new HashSet<>()).add(superSpace);
×
86
        }
×
87
        loaded = true;
×
88
    }
×
89

90
    public static boolean isLoaded() {
91
        return loaded;
×
92
    }
93

94
    public static void ensureLoaded() {
95
        if (spaceList == null) {
2!
96
            refresh(QueryApiAccess.forcedGet(new QueryRef("get-spaces")));
×
97
        }
98
    }
×
99

100
    public static List<Space> getSpaceList() {
101
        ensureLoaded();
×
102
        return spaceList;
×
103
    }
104

105
    public static List<Space> getSpaceList(String type) {
106
        ensureLoaded();
×
107
        return spaceListByType.computeIfAbsent(type, k -> new ArrayList<>());
×
108
    }
109

110
    public static Space get(String id) {
111
        ensureLoaded();
×
112
        return spacesById.get(id);
×
113
    }
114

115
    public static void refresh() {
116
        ensureLoaded();
×
117
        for (Space space : spaceList) {
×
118
            space.dataNeedsUpdate = true;
×
119
        }
×
120
    }
×
121

122
    private String id, label, rootNanopubId, type;
123
    private Nanopub rootNanopub = null;
×
124
    private SpaceData data = new SpaceData();
×
125

126
    private static class SpaceData implements Serializable {
×
127

128
        String description = null;
×
129
        Calendar startDate, endDate;
130
        IRI defaultProvenance = null;
×
131
        List<IRI> admins = new ArrayList<>();
×
132
        List<IRI> members = new ArrayList<>();
×
133
        Map<String,IRI> adminPubkeyMap = new HashMap<>();
×
134
        List<Serializable> pinnedResources = new ArrayList<>();
×
135
        Set<String> pinGroupTags = new HashSet<>();
×
136
        Map<String, List<Serializable>> pinnedResourceMap = new HashMap<>();
×
137

138
        void addAdmin(IRI admin) {
139
            // TODO This isn't efficient for long owner lists:
140
            if (admins.contains(admin)) return;
×
141
            admins.add(admin);
×
142
            UserData ud = User.getUserData();
×
143
            for (String pubkeyhash : ud.getPubkeyhashes(admin, true)) {
×
144
                adminPubkeyMap.put(pubkeyhash, admin);
×
145
            }
×
146
        }
×
147

148
    }
149

150
    private boolean dataInitialized = false;
×
151
    private boolean dataNeedsUpdate = true;
×
152

153
    private Space(ApiResponseEntry resp) {
×
154
        this.id = resp.get("space");
×
155
        this.label = resp.get("label");
×
156
        this.type = resp.get("type");
×
157
        this.rootNanopubId = resp.get("np");
×
158
        this.rootNanopub = Utils.getAsNanopub(rootNanopubId);
×
159
        setCoreData(data);
×
160
    }
×
161

162
    public String getId() {
163
        return id;
×
164
    }
165

166
    public String getRootNanopubId() {
167
        return rootNanopubId;
×
168
    }
169

170
    public String getCoreInfoString() {
171
        return id + " " + rootNanopubId;
×
172
    }
173

174
    public Nanopub getRootNanopub() {
175
        return rootNanopub;
×
176
    }
177

178
    public String getLabel() {
179
        return label;
×
180
    }
181

182
    public String getType() {
183
        return type;
×
184
    }
185

186
    public Calendar getStartDate() {
187
        return data.startDate;
×
188
    }
189

190
    public Calendar getEndDate() {
191
        return data.endDate;
×
192
    }
193

194
    public String getTypeLabel() {
195
        return type.replaceFirst("^.*/", "");
×
196
    }
197

198
    public String getDescription() {
199
        return data.description;
×
200
    }
201

202
    public boolean isDataInitialized() {
203
        triggerDataUpdate();
×
204
        return dataInitialized;
×
205
    }
206

207
    public List<IRI> getAdmins() {
208
        triggerDataUpdate();
×
209
        return data.admins;
×
210
    }
211

212
    public List<IRI> getMembers() {
213
        triggerDataUpdate();
×
214
        return data.members;
×
215
    }
216

217
    public boolean isMember(IRI userId) {
218
        triggerDataUpdate();
×
219
        // TODO This is inefficient for large member lists:
220
        return data.admins.contains(userId) || data.members.contains(userId);
×
221
    }
222

223
    public List<Serializable> getPinnedResources() {
224
        triggerDataUpdate();
×
225
        return data.pinnedResources;
×
226
    }
227

228
    public Set<String> getPinGroupTags() {
229
        triggerDataUpdate();
×
230
        return data.pinGroupTags;
×
231
    }
232

233
    public Map<String, List<Serializable>> getPinnedResourceMap() {
234
        triggerDataUpdate();
×
235
        return data.pinnedResourceMap;
×
236
    }
237

238
    public IRI getDefaultProvenance() {
239
        return data.defaultProvenance;
×
240
    }
241

242
    public String getSuperId() {
243
        return null;
×
244
    }
245

246
    public Space getIdSuperspace() {
247
        if (!id.matches("https?://[^/]+/.*/[^/]*/?")) return null;
×
248
        String superId = id.replaceFirst("(https?://[^/]+/.*)/[^/]*/?", "$1");
×
249
        if (spacesById.containsKey(superId)) {
×
250
            return spacesById.get(superId);
×
251
        }
252
        return null;
×
253
    }
254

255
    public List<Space> getSuperspaces() {
256
        if (superspaceMap.containsKey(this)) {
×
257
            List<Space> superspaces = new ArrayList<>(superspaceMap.get(this));
×
258
            Collections.sort(superspaces, Ordering.usingToString());
×
259
            return superspaces;
×
260
        }
261
        return new ArrayList<>();
×
262
    }
263

264
    public List<Space> getSubspaces() {
265
        if (subspaceMap.containsKey(this)) {
×
266
            List<Space> subspaces = new ArrayList<>(subspaceMap.get(this));
×
267
            Collections.sort(subspaces, Ordering.usingToString());
×
268
            return subspaces;
×
269
        }
270
        return new ArrayList<>();
×
271
    }
272

273
    public List<Space> getSubspaces(String type) {
274
        List<Space> l = new ArrayList<>();
×
275
        for (Space s : getSubspaces()) {
×
276
            if (s.getType().equals(type)) l.add(s);
×
277
        }
×
278
        return l;
×
279
    }
280

281
    private synchronized void triggerDataUpdate() {
282
        if (dataNeedsUpdate) {
×
283
            new Thread(() -> {
×
284
                SpaceData newData = new SpaceData();
×
285
                setCoreData(newData);
×
286

287
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-admins", "unit", id)).getData()) {
×
288
                    String pubkeyhash = r.get("pubkeyhash");
×
289
                    if (newData.adminPubkeyMap.containsKey(pubkeyhash)) {
×
290
                        newData.addAdmin(Utils.vf.createIRI(r.get("admin")));
×
291
                    }
292
                }
×
293
                newData.members = new ArrayList<>();
×
294
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-members", "unit", id)).getData()) {
×
295
                    IRI memberId = Utils.vf.createIRI(r.get("member"));
×
296
                    // TODO These checks are inefficient for long member lists:
297
                    if (newData.admins.contains(memberId)) continue;
×
298
                    if (newData.members.contains(memberId)) continue;
×
299
                    newData.members.add(memberId);
×
300
                }
×
301
                newData.admins.sort(User.getUserData().userComparator);
×
302
                newData.members.sort(User.getUserData().userComparator);
×
303

304
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-pinned-templates", "space", id)).getData()) {
×
305
                    if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
306
                    Template t = TemplateData.get().getTemplate(r.get("template"));
×
307
                    if (t == null) continue;
×
308
                    newData.pinnedResources.add(t);
×
309
                    String tag = r.get("tag");
×
310
                    if (tag != null && !tag.isEmpty()) {
×
311
                        newData.pinGroupTags.add(r.get("tag"));
×
312
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(TemplateData.get().getTemplate(r.get("template")));
×
313
                    }
314
                }
×
315
                for (ApiResponseEntry r : QueryApiAccess.forcedGet(new QueryRef("get-pinned-queries", "space", id)).getData()) {
×
316
                    if (!newData.adminPubkeyMap.containsKey(r.get("pubkey"))) continue;
×
317
                    GrlcQuery query = GrlcQuery.get(r.get("query"));
×
318
                    if (query == null) continue;
×
319
                    newData.pinnedResources.add(query);
×
320
                    String tag = r.get("tag");
×
321
                    if (tag != null && !tag.isEmpty()) {
×
322
                        newData.pinGroupTags.add(r.get("tag"));
×
323
                        newData.pinnedResourceMap.computeIfAbsent(tag, k -> new ArrayList<>()).add(query);
×
324
                    }
325
                }
×
326
                data = newData;
×
327
                dataInitialized = true;
×
328
            }).start();
×
329
            dataNeedsUpdate = false;
×
330
        }
331
    }
×
332

333
    private void setCoreData(SpaceData data) {
334
        for (Statement st : rootNanopub.getAssertion()) {
×
335
            if (st.getSubject().stringValue().equals(getId())) {
×
336
                if (st.getPredicate().equals(DCTERMS.DESCRIPTION)) {
×
337
                    data.description = st.getObject().stringValue();
×
338
                } else if (st.getPredicate().stringValue().equals("http://schema.org/startDate")) {
×
339
                    try {
340
                        data.startDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
×
341
                    } catch (DateTimeParseException ex) {
×
342
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
343
                    }
×
344
                } else if (st.getPredicate().stringValue().equals("http://schema.org/endDate")) {
×
345
                    try {
346
                        data.endDate = DatatypeConverter.parseDateTime(st.getObject().stringValue());
×
347
                    } catch (IllegalArgumentException ex) {
×
348
                        logger.error("Failed to parse date {}", st.getObject().stringValue());
×
349
                    }
×
350
                } else if (st.getPredicate().equals(HAS_ADMIN) && st.getObject() instanceof IRI obj) {
×
351
                    data.addAdmin(obj);
×
352
                } else if (st.getPredicate().equals(HAS_PINNED_TEMPLATE) && st.getObject() instanceof IRI obj) {
×
353
                    data.pinnedResources.add(TemplateData.get().getTemplate(obj.stringValue()));
×
354
                } else if (st.getPredicate().equals(HAS_PINNED_QUERY) && st.getObject() instanceof IRI obj) {
×
355
                    data.pinnedResources.add(GrlcQuery.get(obj.stringValue()));
×
356
                } else if (st.getPredicate().equals(NTEMPLATE.HAS_DEFAULT_PROVENANCE) && st.getObject() instanceof IRI obj) {
×
357
                    data.defaultProvenance = obj;
×
358
                }
359
            } else if (st.getPredicate().equals(NTEMPLATE.HAS_TAG) && st.getObject() instanceof Literal l) {
×
360
                data.pinGroupTags.add(l.stringValue());
×
361
                List<Serializable> list = data.pinnedResourceMap.get(l.stringValue());
×
362
                if (list == null) {
×
363
                    list = new ArrayList<>();
×
364
                    data.pinnedResourceMap.put(l.stringValue(), list);
×
365
                }
366
                list.add(TemplateData.get().getTemplate(st.getSubject().stringValue()));
×
367
            }
368
        }
×
369
    }
×
370

371
    @Override
372
    public String toString() {
373
        return id;
×
374
    }
375

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