Commit | Line | Data |
---|---|---|
1f2a7d5f NR |
1 | package be.nikiroo.fanfix.library; |
2 | ||
59819600 | 3 | import java.io.IOException; |
1f2a7d5f | 4 | import java.util.ArrayList; |
4452446c | 5 | import java.util.Arrays; |
1f2a7d5f | 6 | import java.util.Collections; |
58701825 | 7 | import java.util.Comparator; |
1f2a7d5f | 8 | import java.util.List; |
59819600 NR |
9 | import java.util.Map; |
10 | import java.util.TreeMap; | |
1f2a7d5f NR |
11 | |
12 | import be.nikiroo.fanfix.data.MetaData; | |
59819600 | 13 | import be.nikiroo.utils.StringUtils; |
1f2a7d5f NR |
14 | |
15 | public class MetaResultList { | |
59819600 NR |
16 | /** Max number of items before splitting in [A-B] etc. for eligible items */ |
17 | static private final int MAX = 20; | |
18 | ||
1f2a7d5f NR |
19 | private List<MetaData> metas; |
20 | ||
21 | // Lazy lists: | |
22 | // TODO: sync-protect them? | |
23 | private List<String> sources; | |
24 | private List<String> authors; | |
25 | private List<String> tags; | |
26 | ||
27 | // can be null (will consider it empty) | |
28 | public MetaResultList(List<MetaData> metas) { | |
29 | if (metas == null) { | |
30 | metas = new ArrayList<MetaData>(); | |
31 | } | |
32 | ||
33 | Collections.sort(metas); | |
34 | this.metas = metas; | |
35 | } | |
36 | ||
37 | // not NULL | |
38 | // sorted | |
39 | public List<MetaData> getMetas() { | |
40 | return metas; | |
41 | } | |
42 | ||
43 | public List<String> getSources() { | |
44 | if (sources == null) { | |
45 | sources = new ArrayList<String>(); | |
46 | for (MetaData meta : metas) { | |
47 | if (!sources.contains(meta.getSource())) | |
48 | sources.add(meta.getSource()); | |
49 | } | |
58701825 | 50 | sort(sources); |
1f2a7d5f | 51 | } |
59819600 | 52 | |
1f2a7d5f NR |
53 | return sources; |
54 | } | |
55 | ||
56 | // A -> (A), A/ -> (A, A/*) if we can find something for "*" | |
57 | public List<String> getSources(String source) { | |
58 | List<String> linked = new ArrayList<String>(); | |
59 | if (source != null && !source.isEmpty()) { | |
60 | if (!source.endsWith("/")) { | |
61 | linked.add(source); | |
62 | } else { | |
63 | linked.add(source.substring(0, source.length() - 1)); | |
64 | for (String src : getSources()) { | |
65 | if (src.startsWith(source)) { | |
66 | linked.add(src); | |
67 | } | |
68 | } | |
69 | } | |
70 | } | |
71 | ||
58701825 | 72 | sort(linked); |
1f2a7d5f NR |
73 | return linked; |
74 | } | |
75 | ||
59819600 NR |
76 | /** |
77 | * List all the known types (sources) of stories, grouped by directory | |
78 | * ("Source_1/a" and "Source_1/b" will be grouped into "Source_1"). | |
79 | * <p> | |
80 | * Note that an empty item in the list means a non-grouped source (type) -- | |
81 | * e.g., you could have for Source_1: | |
82 | * <ul> | |
83 | * <li><tt></tt>: empty, so source is "Source_1"</li> | |
84 | * <li><tt>a</tt>: empty, so source is "Source_1/a"</li> | |
85 | * <li><tt>b</tt>: empty, so source is "Source_1/b"</li> | |
86 | * </ul> | |
87 | * | |
88 | * @return the grouped list | |
89 | * | |
90 | * @throws IOException | |
91 | * in case of IOException | |
92 | */ | |
93 | public Map<String, List<String>> getSourcesGrouped() throws IOException { | |
94 | Map<String, List<String>> map = new TreeMap<String, List<String>>(); | |
95 | for (String source : getSources()) { | |
96 | String name; | |
97 | String subname; | |
98 | ||
99 | int pos = source.indexOf('/'); | |
100 | if (pos > 0 && pos < source.length() - 1) { | |
101 | name = source.substring(0, pos); | |
102 | subname = source.substring(pos + 1); | |
103 | ||
104 | } else { | |
105 | name = source; | |
106 | subname = ""; | |
107 | } | |
108 | ||
109 | List<String> list = map.get(name); | |
110 | if (list == null) { | |
111 | list = new ArrayList<String>(); | |
112 | map.put(name, list); | |
113 | } | |
114 | list.add(subname); | |
115 | } | |
116 | ||
117 | return map; | |
118 | } | |
119 | ||
1f2a7d5f NR |
120 | public List<String> getAuthors() { |
121 | if (authors == null) { | |
122 | authors = new ArrayList<String>(); | |
123 | for (MetaData meta : metas) { | |
124 | if (!authors.contains(meta.getAuthor())) | |
125 | authors.add(meta.getAuthor()); | |
126 | } | |
58701825 | 127 | sort(authors); |
1f2a7d5f | 128 | } |
59819600 | 129 | |
1f2a7d5f NR |
130 | return authors; |
131 | } | |
132 | ||
59819600 NR |
133 | /** |
134 | * Return the list of authors, grouped by starting letter(s) if needed. | |
135 | * <p> | |
136 | * If the number of authors is not too high, only one group with an empty | |
137 | * name and all the authors will be returned. | |
138 | * <p> | |
139 | * If not, the authors will be separated into groups: | |
140 | * <ul> | |
141 | * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers | |
142 | * </li> | |
143 | * <li><tt>0-9</tt>: any author whose name starts with a number</li> | |
144 | * <li><tt>A-C</tt> (for instance): any author whose name starts with | |
145 | * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li> | |
146 | * </ul> | |
147 | * Note that the letters used in the groups can vary (except <tt>*</tt> and | |
148 | * <tt>0-9</tt>, which may only be present or not). | |
149 | * | |
150 | * @return the authors' names, grouped by letter(s) | |
151 | * | |
152 | * @throws IOException | |
153 | * in case of IOException | |
154 | */ | |
155 | public Map<String, List<String>> getAuthorsGrouped() throws IOException { | |
156 | return group(getAuthors()); | |
157 | } | |
158 | ||
1f2a7d5f NR |
159 | public List<String> getTags() { |
160 | if (tags == null) { | |
161 | tags = new ArrayList<String>(); | |
162 | for (MetaData meta : metas) { | |
163 | for (String tag : meta.getTags()) { | |
164 | if (!tags.contains(tag)) | |
165 | tags.add(tag); | |
166 | } | |
167 | } | |
58701825 | 168 | sort(tags); |
1f2a7d5f | 169 | } |
59819600 | 170 | |
58701825 | 171 | return tags; |
1f2a7d5f NR |
172 | } |
173 | ||
59819600 NR |
174 | /** |
175 | * Return the list of tags, grouped by starting letter(s) if needed. | |
176 | * <p> | |
177 | * If the number of tags is not too high, only one group with an empty name | |
178 | * and all the tags will be returned. | |
179 | * <p> | |
180 | * If not, the tags will be separated into groups: | |
181 | * <ul> | |
182 | * <li><tt>*</tt>: any tag which name doesn't contain letters nor numbers | |
183 | * </li> | |
184 | * <li><tt>0-9</tt>: any tag which name starts with a number</li> | |
185 | * <li><tt>A-C</tt> (for instance): any tag which name starts with | |
186 | * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li> | |
187 | * </ul> | |
188 | * Note that the letters used in the groups can vary (except <tt>*</tt> and | |
189 | * <tt>0-9</tt>, which may only be present or not). | |
190 | * | |
191 | * @return the tags' names, grouped by letter(s) | |
192 | * | |
193 | * @throws IOException | |
194 | * in case of IOException | |
195 | */ | |
196 | public Map<String, List<String>> getTagsGrouped() throws IOException { | |
197 | return group(getTags()); | |
198 | } | |
199 | ||
4452446c NR |
200 | // helper |
201 | public List<MetaData> filter(String source, String author, String tag) { | |
202 | List<String> sources = source == null ? null : Arrays.asList(source); | |
203 | List<String> authors = author == null ? null : Arrays.asList(author); | |
204 | List<String> tags = tag == null ? null : Arrays.asList(tag); | |
205 | ||
206 | return filter(sources, authors, tags); | |
207 | } | |
208 | ||
1f2a7d5f | 209 | // null or empty -> no check, rest = must be included |
58701825 NR |
210 | // source: a source ending in "/" means "this or any source starting with |
211 | // this", | |
1f2a7d5f NR |
212 | // i;e., to enable source hierarchy |
213 | // + sorted | |
58701825 NR |
214 | public List<MetaData> filter(List<String> sources, List<String> authors, |
215 | List<String> tags) { | |
1f2a7d5f NR |
216 | if (sources != null && sources.isEmpty()) |
217 | sources = null; | |
218 | if (authors != null && authors.isEmpty()) | |
219 | authors = null; | |
220 | if (tags != null && tags.isEmpty()) | |
221 | tags = null; | |
222 | ||
223 | // Quick check | |
224 | if (sources == null && authors == null && tags == null) { | |
225 | return metas; | |
226 | } | |
f433d153 | 227 | |
1f2a7d5f NR |
228 | // allow "sources/" hierarchy |
229 | if (sources != null) { | |
230 | List<String> folders = new ArrayList<String>(); | |
231 | List<String> leaves = new ArrayList<String>(); | |
232 | for (String source : sources) { | |
233 | if (source.endsWith("/")) { | |
234 | if (!folders.contains(source)) | |
235 | folders.add(source); | |
236 | } else { | |
237 | if (!leaves.contains(source)) | |
238 | leaves.add(source); | |
239 | } | |
240 | } | |
241 | ||
242 | sources = leaves; | |
243 | for (String folder : folders) { | |
244 | for (String otherLeaf : getSources(folder)) { | |
245 | if (!sources.contains(otherLeaf)) { | |
246 | sources.add(otherLeaf); | |
247 | } | |
248 | } | |
249 | } | |
250 | } | |
251 | ||
252 | List<MetaData> result = new ArrayList<MetaData>(); | |
253 | for (MetaData meta : metas) { | |
254 | if (sources != null && !sources.contains(meta.getSource())) { | |
255 | continue; | |
256 | } | |
257 | if (authors != null && !authors.contains(meta.getAuthor())) { | |
258 | continue; | |
259 | } | |
260 | ||
261 | if (tags != null) { | |
262 | boolean keep = false; | |
263 | for (String thisTag : meta.getTags()) { | |
264 | if (tags.contains(thisTag)) | |
265 | keep = true; | |
266 | } | |
267 | ||
268 | if (!keep) | |
269 | continue; | |
270 | } | |
271 | ||
272 | result.add(meta); | |
273 | } | |
274 | ||
275 | Collections.sort(result); | |
276 | return result; | |
277 | } | |
59819600 NR |
278 | |
279 | /** | |
280 | * Return the list of values, grouped by starting letter(s) if needed. | |
281 | * <p> | |
282 | * If the number of values is not too high, only one group with an empty | |
283 | * name and all the values will be returned (see | |
284 | * {@link MetaResultList#MAX}). | |
285 | * <p> | |
286 | * If not, the values will be separated into groups: | |
287 | * <ul> | |
288 | * <li><tt>*</tt>: any value which name doesn't contain letters nor numbers | |
289 | * </li> | |
290 | * <li><tt>0-9</tt>: any value which name starts with a number</li> | |
291 | * <li><tt>A-C</tt> (for instance): any value which name starts with | |
292 | * <tt>A</tt>, <tt>B</tt> or <tt>C</tt></li> | |
293 | * </ul> | |
294 | * Note that the letters used in the groups can vary (except <tt>*</tt> and | |
295 | * <tt>0-9</tt>, which may only be present or not). | |
296 | * | |
297 | * @param values | |
298 | * the values to group | |
299 | * | |
300 | * @return the values, grouped by letter(s) | |
301 | * | |
302 | * @throws IOException | |
303 | * in case of IOException | |
304 | */ | |
305 | private Map<String, List<String>> group(List<String> values) | |
306 | throws IOException { | |
307 | Map<String, List<String>> groups = new TreeMap<String, List<String>>(); | |
308 | ||
309 | // If all authors fit the max, just report them as is | |
310 | if (values.size() <= MAX) { | |
311 | groups.put("", values); | |
312 | return groups; | |
313 | } | |
314 | ||
315 | // Create groups A to Z, which can be empty here | |
316 | for (char car = 'A'; car <= 'Z'; car++) { | |
317 | groups.put(Character.toString(car), find(values, car)); | |
318 | } | |
319 | ||
320 | // Collapse them | |
321 | List<String> keys = new ArrayList<String>(groups.keySet()); | |
322 | for (int i = 0; i + 1 < keys.size(); i++) { | |
323 | String keyNow = keys.get(i); | |
324 | String keyNext = keys.get(i + 1); | |
325 | ||
326 | List<String> now = groups.get(keyNow); | |
327 | List<String> next = groups.get(keyNext); | |
328 | ||
329 | int currentTotal = now.size() + next.size(); | |
330 | if (currentTotal <= MAX) { | |
331 | String key = keyNow.charAt(0) + "-" | |
332 | + keyNext.charAt(keyNext.length() - 1); | |
333 | ||
334 | List<String> all = new ArrayList<String>(); | |
335 | all.addAll(now); | |
336 | all.addAll(next); | |
337 | ||
338 | groups.remove(keyNow); | |
339 | groups.remove(keyNext); | |
340 | groups.put(key, all); | |
341 | ||
342 | keys.set(i, key); // set the new key instead of key(i) | |
343 | keys.remove(i + 1); // remove the next, consumed key | |
344 | i--; // restart at key(i) | |
345 | } | |
346 | } | |
347 | ||
348 | // Add "special" groups | |
349 | groups.put("*", find(values, '*')); | |
350 | groups.put("0-9", find(values, '0')); | |
351 | ||
352 | // Prune empty groups | |
353 | keys = new ArrayList<String>(groups.keySet()); | |
354 | for (String key : keys) { | |
355 | if (groups.get(key).isEmpty()) { | |
356 | groups.remove(key); | |
357 | } | |
358 | } | |
359 | ||
360 | return groups; | |
361 | } | |
362 | ||
363 | /** | |
364 | * Get all the authors that start with the given character: | |
365 | * <ul> | |
366 | * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers | |
367 | * </li> | |
368 | * <li><tt>0</tt>: any authors whose name starts with a number</li> | |
369 | * <li><tt>A</tt> (any capital latin letter): any author whose name starts | |
370 | * with <tt>A</tt></li> | |
371 | * </ul> | |
372 | * | |
373 | * @param values | |
374 | * the full list of authors | |
375 | * @param car | |
376 | * the starting character, <tt>*</tt>, <tt>0</tt> or a capital | |
377 | * letter | |
378 | * | |
379 | * @return the authors that fulfil the starting letter | |
380 | */ | |
381 | private List<String> find(List<String> values, char car) { | |
382 | List<String> accepted = new ArrayList<String>(); | |
383 | for (String value : values) { | |
384 | char first = '*'; | |
385 | for (int i = 0; first == '*' && i < value.length(); i++) { | |
386 | String san = StringUtils.sanitize(value, true, true); | |
387 | char c = san.charAt(i); | |
388 | if (c >= '0' && c <= '9') { | |
389 | first = '0'; | |
390 | } else if (c >= 'a' && c <= 'z') { | |
391 | first = (char) (c - 'a' + 'A'); | |
392 | } else if (c >= 'A' && c <= 'Z') { | |
393 | first = c; | |
394 | } | |
395 | } | |
396 | ||
397 | if (first == car) { | |
398 | accepted.add(value); | |
399 | } | |
400 | } | |
401 | ||
402 | return accepted; | |
403 | } | |
404 | ||
58701825 NR |
405 | /** |
406 | * Sort the given {@link String} values, ignoring case. | |
407 | * | |
408 | * @param values | |
409 | * the values to sort | |
410 | */ | |
411 | private void sort(List<String> values) { | |
412 | Collections.sort(values, new Comparator<String>() { | |
413 | @Override | |
414 | public int compare(String o1, String o2) { | |
415 | return ("" + o1).compareToIgnoreCase("" + o2); | |
416 | } | |
417 | }); | |
418 | } | |
1f2a7d5f | 419 | } |