Merge commit '712ddafb749aada41daab85c36ac12f657b2307e'
[fanfix.git] / src / be / nikiroo / fanfix / library / MetaResultList.java
1 package be.nikiroo.fanfix.library;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Collections;
7 import java.util.Comparator;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.TreeMap;
11
12 import be.nikiroo.fanfix.data.MetaData;
13 import be.nikiroo.utils.StringUtils;
14
15 public class MetaResultList {
16 /** Max number of items before splitting in [A-B] etc. for eligible items */
17 static private final int MAX = 20;
18
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 }
50 sort(sources);
51 }
52
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
72 sort(linked);
73 return linked;
74 }
75
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
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 }
127 sort(authors);
128 }
129
130 return authors;
131 }
132
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
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 }
168 sort(tags);
169 }
170
171 return tags;
172 }
173
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
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
209 // null or empty -> no check, rest = must be included
210 // source: a source ending in "/" means "this or any source starting with
211 // this",
212 // i;e., to enable source hierarchy
213 // + sorted
214 public List<MetaData> filter(List<String> sources, List<String> authors,
215 List<String> tags) {
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 }
227
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 }
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
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 }
419 }