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

knowledgepixels / nanodash / 37889468619

09 Oct 2026 05:37AM UTC coverage: 51.491% (+0.08%) from 51.411%
37889468619

Pull #771

github

web-flow
Merge 62f065e74 into 108b57d49
Pull Request #771: Group result-table rows under header rows via a "_group" column

5554 of 11589 branches covered (47.92%)

Branch coverage included in aggregate %.

9829 of 18286 relevant lines covered (53.75%)

8.72 hits per line

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

61.79
src/main/java/com/knowledgepixels/nanodash/FilteredQueryResultDataProvider.java
1
package com.knowledgepixels.nanodash;
2

3
import org.apache.wicket.extensions.markup.html.repeater.data.sort.ISortState;
4
import org.apache.wicket.extensions.markup.html.repeater.data.table.ISortableDataProvider;
5
import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
6
import org.apache.wicket.model.IModel;
7
import org.nanopub.extra.services.ApiResponse;
8
import org.nanopub.extra.services.ApiResponseEntry;
9

10
import java.util.ArrayList;
11
import java.util.Arrays;
12
import java.util.HashMap;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.Map;
16

17
/**
18
 * Filtered data provider that wraps QueryResultDataProvider and filters results based on a filter string.
19
 */
20
public class FilteredQueryResultDataProvider implements ISortableDataProvider<ApiResponseEntry, String> {
21

22
    private final QueryResultDataProvider baseProvider;
23
    private final ApiResponse response;
24
    private String filterText = "";
9 ✔
25
    private List<ApiResponseEntry> filteredData = null;
9 ✔
26

27
    public FilteredQueryResultDataProvider(QueryResultDataProvider baseProvider, ApiResponse response) {
6 ✔
28
        this.baseProvider = baseProvider;
9 ✔
29
        this.response = response;
9 ✔
30
    }
3 ✔
31

32
    public void setFilterText(String filterText) {
33
        if (filterText == null) {
×
34
            filterText = "";
×
35
        }
36
        if (!this.filterText.equals(filterText)) {
×
37
            this.filterText = filterText;
×
38
            this.filteredData = null; // Invalidate cache
×
39
        }
40
    }
×
41

42
    public List<ApiResponseEntry> getFilteredData() {
43
        if (filteredData != null) {
9 ✔
44
            return filteredData;
9 ✔
45
        }
46

47
        List<ApiResponseEntry> allData = response.getData();
12 ✔
48
        if (filterText == null || filterText.trim().isEmpty()) {
24 !
49
            filteredData = allData;
12 ✔
50
        } else {
51
            String lowerFilter = filterText.toLowerCase();
×
52
            filteredData = new ArrayList<>();
×
53
            for (ApiResponseEntry entry : allData) {
×
54
                boolean matches = false;
×
55
                for (String key : response.getHeader()) {
×
56
                    String value = entry.get(key);
×
57
                    if (value != null && value.toLowerCase().contains(lowerFilter)) {
×
58
                        matches = true;
×
59
                        break;
×
60
                    }
61
                }
62
                if (matches) {
×
63
                    filteredData.add(entry);
×
64
                }
65
            }
×
66
        }
67
        return filteredData;
9 ✔
68
    }
69

70
    /**
71
     * The name of the "_group" column of this result, if any: the column whose values
72
     * group the rows into sections (rendered as header rows by the result table).
73
     *
74
     * @return the column name, or null when the result has no such column
75
     */
76
    public String getGroupColumn() {
77
        for (String h : response.getHeader()) {
54 ✔
78
            if (h.endsWith("_group")) return h;
18 ✔
79
        }
80
        return null;
6 ✔
81
    }
82

83
    /**
84
     * The rank of each group value: its position among the distinct values of the
85
     * "_group" column in the order the query returned them. Column sorting keeps the
86
     * sections in that order and only reorders the rows inside each section.
87
     */
88
    private Map<String, Integer> groupRanks() {
89
        String groupColumn = getGroupColumn();
9 ✔
90
        Map<String, Integer> ranks = new HashMap<>();
12 ✔
91
        if (groupColumn == null) return ranks;
12 ✔
92
        for (ApiResponseEntry entry : response.getData()) {
36 ✔
93
            String g = entry.get(groupColumn);
12 ✔
94
            if (g == null) g = "";
6 !
95
            ranks.putIfAbsent(g, ranks.size());
21 ✔
96
        }
3 ✔
97
        return ranks;
6 ✔
98
    }
99

100
    @Override
101
    public Iterator<? extends ApiResponseEntry> iterator(long first, long count) {
102
        List<ApiResponseEntry> data = new ArrayList<>(getFilteredData());
18 ✔
103
        SortParam<String> sortParam = baseProvider.getSortParam();
12 ✔
104
        if (sortParam != null) {
6 ✔
105
            String prop = sortParam.getProperty();
12 ✔
106
            String labelProp = prop + "_label";
9 ✔
107
            String sortProp = Arrays.asList(response.getHeader()).contains(labelProp) ? labelProp : prop;
27 !
108
            // With a "_group" column, the sections keep their query order whichever
109
            // column is sorted and in whichever direction; the sort applies within them.
110
            String groupColumn = getGroupColumn();
9 ✔
111
            Map<String, Integer> groupRanks = groupRanks();
9 ✔
112
            // Values are compared with Utils.compareValues rather than as plain text, so
113
            // that a column of numbers does not come out with "10" above "9" (issue #673).
114
            data.sort((o1, o2) -> {
21 ✔
115
                if (groupColumn != null) {
6 ✔
116
                    String g1 = o1.get(groupColumn);
12 ✔
117
                    String g2 = o2.get(groupColumn);
12 ✔
118
                    int byGroup = Integer.compare(groupRanks.getOrDefault(g1 == null ? "" : g1, Integer.MAX_VALUE),
36 !
119
                            groupRanks.getOrDefault(g2 == null ? "" : g2, Integer.MAX_VALUE));
24 !
120
                    if (byGroup != 0) return byGroup;
12 ✔
121
                }
122
                String v1 = o1.get(sortProp);
12 ✔
123
                String v2 = o2.get(sortProp);
12 ✔
124
                int result;
125
                if (v1 == null && v2 == null) result = 0;
6 !
126
                else if (v1 == null) result = 1;
6 !
127
                else if (v2 == null) result = -1;
6 !
128
                else result = Utils.compareValues(v1, v2);
12 ✔
129
                if (!sortParam.isAscending()) result = -result;
18 ✔
130
                return result;
6 ✔
131
            });
132
        }
133
        return Utils.subList(data, first, first + count).iterator();
24 ✔
134
    }
135

136
    @Override
137
    public IModel<ApiResponseEntry> model(ApiResponseEntry object) {
138
        return baseProvider.model(object);
×
139
    }
140

141
    @Override
142
    public long size() {
143
        return getFilteredData().size();
15 ✔
144
    }
145

146
    @Override
147
    public ISortState<String> getSortState() {
148
        return baseProvider.getSortState();
×
149
    }
150

151
    @Override
152
    public void detach() {
153
        baseProvider.detach();
×
154
    }
×
155

156
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc