1 package be
.nikiroo
.fanfix
.library
;
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
;
10 import java
.util
.TreeMap
;
12 import be
.nikiroo
.fanfix
.data
.MetaData
;
13 import be
.nikiroo
.utils
.StringUtils
;
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;
19 private List
<MetaData
> metas
;
22 // TODO: sync-protect them?
23 private List
<String
> sources
;
24 private List
<String
> authors
;
25 private List
<String
> tags
;
27 // can be null (will consider it empty)
28 public MetaResultList(List
<MetaData
> metas
) {
30 metas
= new ArrayList
<MetaData
>();
33 Collections
.sort(metas
);
39 public List
<MetaData
> getMetas() {
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());
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("/")) {
63 linked
.add(source
.substring(0, source
.length() - 1));
64 for (String src
: getSources()) {
65 if (src
.startsWith(source
)) {
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").
80 * Note that an empty item in the list means a non-grouped source (type) --
81 * e.g., you could have for Source_1:
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>
88 * @return the grouped list
91 * in case of IOException
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()) {
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);
109 List
<String
> list
= map
.get(name
);
111 list
= new ArrayList
<String
>();
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());
134 * Return the list of authors, grouped by starting letter(s) if needed.
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.
139 * If not, the authors will be separated into groups:
141 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
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).
150 * @return the authors' names, grouped by letter(s)
152 * @throws IOException
153 * in case of IOException
155 public Map
<String
, List
<String
>> getAuthorsGrouped() throws IOException
{
156 return group(getAuthors());
159 public List
<String
> getTags() {
161 tags
= new ArrayList
<String
>();
162 for (MetaData meta
: metas
) {
163 for (String tag
: meta
.getTags()) {
164 if (!tags
.contains(tag
))
175 * Return the list of tags, grouped by starting letter(s) if needed.
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.
180 * If not, the tags will be separated into groups:
182 * <li><tt>*</tt>: any tag which name doesn't contain letters nor numbers
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>
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).
191 * @return the tags' names, grouped by letter(s)
193 * @throws IOException
194 * in case of IOException
196 public Map
<String
, List
<String
>> getTagsGrouped() throws IOException
{
197 return group(getTags());
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
);
206 return filter(sources
, authors
, tags
);
209 // null or empty -> no check, rest = must be included
210 // source: a source ending in "/" means "this or any source starting with
212 // i;e., to enable source hierarchy
214 public List
<MetaData
> filter(List
<String
> sources
, List
<String
> authors
,
216 if (sources
!= null && sources
.isEmpty())
218 if (authors
!= null && authors
.isEmpty())
220 if (tags
!= null && tags
.isEmpty())
224 if (sources
== null && authors
== null && tags
== null) {
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
))
237 if (!leaves
.contains(source
))
243 for (String folder
: folders
) {
244 for (String otherLeaf
: getSources(folder
)) {
245 if (!sources
.contains(otherLeaf
)) {
246 sources
.add(otherLeaf
);
252 List
<MetaData
> result
= new ArrayList
<MetaData
>();
253 for (MetaData meta
: metas
) {
254 if (sources
!= null && !sources
.contains(meta
.getSource())) {
257 if (authors
!= null && !authors
.contains(meta
.getAuthor())) {
262 boolean keep
= false;
263 for (String thisTag
: meta
.getTags()) {
264 if (tags
.contains(thisTag
))
275 Collections
.sort(result
);
280 * Return the list of values, grouped by starting letter(s) if needed.
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}).
286 * If not, the values will be separated into groups:
288 * <li><tt>*</tt>: any value which name doesn't contain letters nor numbers
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>
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).
298 * the values to group
300 * @return the values, grouped by letter(s)
302 * @throws IOException
303 * in case of IOException
305 private Map
<String
, List
<String
>> group(List
<String
> values
)
307 Map
<String
, List
<String
>> groups
= new TreeMap
<String
, List
<String
>>();
309 // If all authors fit the max, just report them as is
310 if (values
.size() <= MAX
) {
311 groups
.put("", values
);
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
));
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);
326 List
<String
> now
= groups
.get(keyNow
);
327 List
<String
> next
= groups
.get(keyNext
);
329 int currentTotal
= now
.size() + next
.size();
330 if (currentTotal
<= MAX
) {
331 String key
= keyNow
.charAt(0) + "-"
332 + keyNext
.charAt(keyNext
.length() - 1);
334 List
<String
> all
= new ArrayList
<String
>();
338 groups
.remove(keyNow
);
339 groups
.remove(keyNext
);
340 groups
.put(key
, all
);
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)
348 // Add "special" groups
349 groups
.put("*", find(values
, '*'));
350 groups
.put("0-9", find(values
, '0'));
352 // Prune empty groups
353 keys
= new ArrayList
<String
>(groups
.keySet());
354 for (String key
: keys
) {
355 if (groups
.get(key
).isEmpty()) {
364 * Get all the authors that start with the given character:
366 * <li><tt>*</tt>: any author whose name doesn't contain letters nor numbers
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>
374 * the full list of authors
376 * the starting character, <tt>*</tt>, <tt>0</tt> or a capital
379 * @return the authors that fulfil the starting letter
381 private List
<String
> find(List
<String
> values
, char car
) {
382 List
<String
> accepted
= new ArrayList
<String
>();
383 for (String value
: values
) {
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') {
390 } else if (c
>= 'a' && c
<= 'z') {
391 first
= (char) (c
- 'a' + 'A');
392 } else if (c
>= 'A' && c
<= 'Z') {
406 * Sort the given {@link String} values, ignoring case.
411 private void sort(List
<String
> values
) {
412 Collections
.sort(values
, new Comparator
<String
>() {
414 public int compare(String o1
, String o2
) {
415 return ("" + o1
).compareToIgnoreCase("" + o2
);