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

knowledgepixels / nanodash / 23138432878

16 Mar 2026 10:09AM UTC coverage: 15.99% (+0.2%) from 15.811%
23138432878

push

github

web-flow
Merge pull request #402 from knowledgepixels/fix/401-bounded-api-cache

Fix unbounded memory growth and resource exhaustion

717 of 5509 branches covered (13.02%)

Branch coverage included in aggregate %.

1810 of 10295 relevant lines covered (17.58%)

2.39 hits per line

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

0.0
src/main/java/com/knowledgepixels/nanodash/component/GuidedChoiceItem.java
1
package com.knowledgepixels.nanodash.component;
2

3
import com.knowledgepixels.nanodash.LocalUri;
4
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
5
import com.knowledgepixels.nanodash.Utils;
6
import com.knowledgepixels.nanodash.component.IriTextfieldItem.Validator;
7
import com.knowledgepixels.nanodash.template.Template;
8
import com.knowledgepixels.nanodash.template.TemplateContext;
9
import com.knowledgepixels.nanodash.template.UnificationException;
10
import org.apache.wicket.Component;
11
import org.apache.wicket.ajax.AjaxRequestTarget;
12
import org.apache.wicket.ajax.form.OnChangeAjaxBehavior;
13
import org.apache.wicket.behavior.AttributeAppender;
14
import org.apache.wicket.markup.html.basic.Label;
15
import org.apache.wicket.markup.html.link.ExternalLink;
16
import org.apache.wicket.model.IModel;
17
import org.apache.wicket.model.Model;
18
import org.apache.wicket.validation.Validatable;
19
import org.eclipse.rdf4j.model.IRI;
20
import org.eclipse.rdf4j.model.Value;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23
import org.wicketstuff.select2.ChoiceProvider;
24
import org.wicketstuff.select2.Response;
25
import org.wicketstuff.select2.Select2Choice;
26

27
import com.google.common.cache.Cache;
28
import com.google.common.cache.CacheBuilder;
29

30
import java.util.*;
31
import java.util.concurrent.TimeUnit;
32

33
/**
34
 * A guided choice item that allows users to select from a list of predefined values.
35
 */
36
public class GuidedChoiceItem extends AbstractContextComponent {
37

38
    private Select2Choice<String> textfield;
39
    private ExternalLink tooltipLink;
40
    private Label tooltipDescription;
41
    private IRI iri;
42
    private IModel<String> model;
43
    private static final Logger logger = LoggerFactory.getLogger(GuidedChoiceItem.class);
×
44

45
    private String prefix;
46

47
    // TODO: This map being static could mix up labels if the same URI is described at different places:
48
    // TODO: This should maybe go into a different class?
49
    private static final Cache<String, String> labelCache = CacheBuilder.newBuilder()
×
50
        .maximumSize(50_000)
×
51
        .expireAfterAccess(24, TimeUnit.HOURS)
×
52
        .build();
×
53
    private static final Map<String, String> labelMap = labelCache.asMap();
×
54

55
    /**
56
     * Get the label for a given value.
57
     *
58
     * @param value The value for which to get the label.
59
     * @return The label associated with the value, or null if not found.
60
     */
61
    public static String getLabel(String value) {
62
        return labelMap.get(value);
×
63
    }
64

65
    /**
66
     * Set a label for a given value.
67
     *
68
     * @param value The value for which to set the label.
69
     * @param label The label to associate with the value.
70
     * @return The previous label associated with the value, or null if there was none.
71
     */
72
    public static String setLabel(String value, String label) {
73
        return labelMap.put(value, label);
×
74
    }
75

76
    private String getChoiceLabel(String choiceId) {
77
        Template template = context.getTemplate();
×
78
        String label = null;
×
79
        if (choiceId.matches("https?://.+") && template.getLabel(vf.createIRI(choiceId)) != null) {
×
80
            label = template.getLabel(vf.createIRI(choiceId));
×
81
        } else if (labelMap.containsKey(choiceId)) {
×
82
            label = labelMap.get(choiceId);
×
83
            if (label.length() > 160) label = label.substring(0, 157) + "...";
×
84
        }
85
        return label;
×
86
    }
87

88
    /**
89
     * Constructor for the GuidedChoiceItem.
90
     *
91
     * @param id       The Wicket component ID.
92
     * @param parentId The parent ID, used for context.
93
     * @param iriP     The IRI associated with this choice item.
94
     * @param optional Whether the choice is optional or required.
95
     * @param context  The template context containing the template and models.
96
     */
97
    public GuidedChoiceItem(String id, String parentId, final IRI iriP, boolean optional, final TemplateContext context) {
98
        super(id, context);
×
99
        this.iri = iriP;
×
100
        final Template template = context.getTemplate();
×
101
        model = (IModel<String>) context.getComponentModels().get(iri);
×
102
        if (model == null) {
×
103
            model = Model.of("");
×
104
            context.getComponentModels().put(iri, model);
×
105
        }
106
        String postfix = Utils.getUriPostfix(iri);
×
107
        if (context.hasParam(postfix)) {
×
108
            String objId = context.getParam(postfix);
×
109
            if (AbstractResourceWithProfile.get(objId) != null && AbstractResourceWithProfile.get(objId).getLabel() != null) {
×
110
                labelMap.put(objId, AbstractResourceWithProfile.get(objId).getLabel());
×
111
            }
112
            model.setObject(objId);
×
113
        }
114
        final List<String> possibleValues = new ArrayList<>();
×
115
        for (Value v : template.getPossibleValues(iri)) {
×
116
            possibleValues.add(v.toString());
×
117
        }
×
118

119
        prefix = template.getPrefix(iri);
×
120
        if (prefix == null) prefix = "";
×
121
        String prefixLabel = template.getPrefixLabel(iri);
×
122
        Label prefixLabelComp;
123
        if (prefixLabel == null) {
×
124
            prefixLabelComp = new Label("prefix", "");
×
125
            prefixLabelComp.setVisible(false);
×
126
        } else {
127
            if (!prefixLabel.isEmpty() && parentId.equals("subj") && !prefixLabel.matches("https?://.*")) {
×
128
                // Capitalize first letter of label if at subject position:
129
                prefixLabel = prefixLabel.substring(0, 1).toUpperCase() + prefixLabel.substring(1);
×
130
            }
131
            prefixLabelComp = new Label("prefix", prefixLabel);
×
132
        }
133
        add(prefixLabelComp);
×
134
        String prefixTooltip = prefix;
×
135
        if (!prefix.isEmpty()) {
×
136
            prefixTooltip += "...";
×
137
        }
138
        add(new Label("prefixtooltiptext", prefixTooltip));
×
139

140
        ChoiceProvider<String> choiceProvider = new ChoiceProvider<String>() {
×
141

142
            @Override
143
            public String getDisplayValue(String choiceId) {
144
                if (choiceId == null || choiceId.isEmpty()) return "";
×
145
                String label = getChoiceLabel(choiceId);
×
146
                if (label == null || label.isBlank()) {
×
147
                    return choiceId;
×
148
                }
149
                return label + " (" + choiceId + ")";
×
150
            }
151

152
            @Override
153
            public String getIdValue(String object) {
154
                return object;
×
155
            }
156

157
            // Getting strange errors with Tomcat if this method is not overridden:
158
            @Override
159
            public void detach() {
160
            }
×
161

162
            @Override
163
            public void query(String term, int page, Response<String> response) {
164
                if (term == null) {
×
165
                    response.addAll(possibleValues);
×
166
                    return;
×
167
                }
168
                if (term.startsWith("https://") || term.startsWith("http://")) {
×
169
                    if (prefix == null || term.startsWith(prefix)) {
×
170
                        response.add(term);
×
171
                    }
172
                }
173
                Map<String, Boolean> alreadyAddedMap = new HashMap<>();
×
174
                term = term.toLowerCase();
×
175
                for (String s : possibleValues) {
×
176
                    if (s.toLowerCase().contains(term) || getDisplayValue(s).toLowerCase().contains(term)) {
×
177
                        response.add(s);
×
178
                        alreadyAddedMap.put(s, true);
×
179
                    }
180
                }
×
181
                for (String v : context.getTemplate().getPossibleValuesFromApi(iri, term, labelMap)) {
×
182
                    if (!alreadyAddedMap.containsKey(v)) response.add(v);
×
183
                }
×
184
            }
×
185

186
            @Override
187
            public Collection<String> toChoices(Collection<String> ids) {
188
                return ids;
×
189
            }
190

191
        };
192
        textfield = new Select2Choice<String>("textfield", model, choiceProvider);
×
193
        textfield.getSettings().getAjax(true).setDelay(500);
×
194
        textfield.getSettings().setCloseOnSelect(true);
×
195
        String placeholder = template.getLabel(iri);
×
196
        if (placeholder == null) placeholder = "";
×
197
        textfield.getSettings().setPlaceholder(placeholder);
×
198
        Utils.setSelect2ChoiceMinimalEscapeMarkup(textfield);
×
199
        textfield.getSettings().setAllowClear(true);
×
200

201
        if (!optional) textfield.setRequired(true);
×
202
        textfield.add(new AttributeAppender("class", " wide"));
×
203
        textfield.add(new Validator(iri, template, prefix, context));
×
204
        context.getComponents().add(textfield);
×
205

206
        tooltipDescription = new Label("description", new IModel<String>() {
×
207

208
            @Override
209
            public String getObject() {
210
                String obj = GuidedChoiceItem.this.getModel().getObject();
×
211
                if (obj == null || obj.isEmpty()) return "choose a value";
×
212
                String label = getChoiceLabel(GuidedChoiceItem.this.getModel().getObject());
×
213
                if (label == null || !label.contains(" - ")) return "";
×
214
                return label.substring(label.indexOf(" - ") + 3);
×
215
            }
216

217
        });
218
        tooltipDescription.setOutputMarkupId(true);
×
219
        add(tooltipDescription);
×
220

221
        tooltipLink = Utils.getUriLink("uri", model);
×
222
        tooltipLink.setOutputMarkupId(true);
×
223
        add(tooltipLink);
×
224

225
        textfield.add(new OnChangeAjaxBehavior() {
×
226

227
            @Override
228
            protected void onUpdate(AjaxRequestTarget target) {
229
                for (Component c : context.getComponents()) {
×
230
                    if (c == textfield) continue;
×
231
                    if (c.getDefaultModel() == textfield.getModel()) {
×
232
                        c.modelChanged();
×
233
                        target.add(c);
×
234
                    }
235
                }
×
236
                target.add(tooltipLink);
×
237
                target.add(tooltipDescription);
×
238
            }
×
239

240
        });
241
        add(textfield);
×
242
    }
×
243

244
    /**
245
     * Get the model associated with this choice item.
246
     *
247
     * @return The model containing the selected value.
248
     */
249
    public IModel<String> getModel() {
250
        return model;
×
251
    }
252

253
    /**
254
     * {@inheritDoc}
255
     */
256
    @Override
257
    public void removeFromContext() {
258
        context.getComponents().remove(textfield);
×
259
    }
×
260

261
    /**
262
     * {@inheritDoc}
263
     */
264
    @Override
265
    public boolean isUnifiableWith(Value v) {
266
        if (v == null) return true;
×
267
        if (v instanceof IRI) {
×
268
            String vs = v.stringValue();
×
269
            if (vs.startsWith(prefix)) vs = vs.substring(prefix.length());
×
270
            if (Utils.isLocalURI(vs)) vs = vs.replaceFirst("^" + LocalUri.PREFIX, "");
×
271
            Validatable<String> validatable = new Validatable<>(vs);
×
272
            if (context.getTemplate().isLocalResource(iri) && !Utils.isUriPostfix(vs)) {
×
273
                vs = Utils.getUriPostfix(vs);
×
274
            }
275
            new Validator(iri, context.getTemplate(), prefix, context).validate(validatable);
×
276
            if (!validatable.isValid()) {
×
277
                return false;
×
278
            }
279
            if (textfield.getModelObject() == null || textfield.getModelObject().isEmpty()) {
×
280
                return true;
×
281
            }
282
            return vs.equals(textfield.getModelObject());
×
283
        }
284
        return false;
×
285
    }
286

287
    /**
288
     * {@inheritDoc}
289
     */
290
    @Override
291
    public void unifyWith(Value v) throws UnificationException {
292
        if (v == null) return;
×
293
        if (!isUnifiableWith(v)) throw new UnificationException(v.stringValue());
×
294
        String vs = v.stringValue();
×
295
        if (prefix != null && vs.startsWith(prefix)) {
×
296
            vs = vs.substring(prefix.length());
×
297
        } else if (Utils.isLocalURI(vs)) {
×
298
            vs = vs.replaceFirst("^" + LocalUri.PREFIX, "");
×
299
        }
300
        textfield.setModelObject(vs);
×
301
        // TODO: This should be done differently, at a different place (can slow down unification):
302
        if (!labelMap.containsKey(vs)) {
×
303
            context.getTemplate().getPossibleValuesFromApi(iri, vs, labelMap);
×
304
        }
305
    }
×
306

307
    /**
308
     * {@inheritDoc}
309
     */
310
    @Override
311
    public void fillFinished() {
312
    }
×
313

314
    /**
315
     * {@inheritDoc}
316
     */
317
    @Override
318
    public void finalizeValues() {
319
        Value defaultValue = context.getTemplate().getDefault(iri);
×
320
        if (isUnifiableWith(defaultValue)) {
×
321
            try {
322
                unifyWith(defaultValue);
×
323
            } catch (UnificationException ex) {
×
324
                logger.error("Could not unify default value: {}", defaultValue, ex);
×
325
            }
×
326
        }
327
    }
×
328

329
    /**
330
     * {@inheritDoc}
331
     */
332
    @Override
333
    public String toString() {
334
        return "[Guided choiced item: " + iri + "]";
×
335
    }
336

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