Version 1.4.2
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
1 package be.nikiroo.fanfix;
2
3 import java.io.File;
4 import java.io.FileFilter;
5 import java.io.IOException;
6 import java.net.URL;
7 import java.util.ArrayList;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.List;
11 import java.util.Map;
12 import java.util.Map.Entry;
13
14 import be.nikiroo.fanfix.bundles.Config;
15 import be.nikiroo.fanfix.data.MetaData;
16 import be.nikiroo.fanfix.data.Story;
17 import be.nikiroo.fanfix.output.BasicOutput;
18 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
19 import be.nikiroo.fanfix.supported.BasicSupport;
20 import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
21 import be.nikiroo.fanfix.supported.InfoReader;
22 import be.nikiroo.utils.IOUtils;
23 import be.nikiroo.utils.Progress;
24
25 /**
26 * Manage a library of Stories: import, export, list.
27 * <p>
28 * Each {@link Story} object will be associated with a (local to the library)
29 * unique ID, the LUID, which will be used to identify the {@link Story}.
30 *
31 * @author niki
32 */
33 public class Library {
34 private File baseDir;
35 private Map<MetaData, File> stories;
36 private int lastId;
37 private OutputType text;
38 private OutputType image;
39
40 /**
41 * Create a new {@link Library} with the given backend directory.
42 *
43 * @param dir
44 * the directory where to find the {@link Story} objects
45 * @param text
46 * the {@link OutputType} to save the text-focused stories into
47 * @param image
48 * the {@link OutputType} to save the images-focused stories into
49 */
50 public Library(File dir, OutputType text, OutputType image) {
51 this.baseDir = dir;
52 this.stories = new HashMap<MetaData, File>();
53 this.lastId = 0;
54 this.text = text;
55 this.image = image;
56
57 dir.mkdirs();
58 }
59
60 /**
61 * Refresh the {@link Library}, that is, make sure all stories are loaded.
62 *
63 * @param pg
64 * the optional progress reporter
65 */
66 public void refresh(Progress pg) {
67 getStories(pg);
68 }
69
70 /**
71 * List all the known types of stories.
72 *
73 * @return the types
74 */
75 public synchronized List<String> getTypes() {
76 List<String> list = new ArrayList<String>();
77 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
78 String storyType = entry.getKey().getSource();
79 if (!list.contains(storyType)) {
80 list.add(storyType);
81 }
82 }
83
84 Collections.sort(list);
85 return list;
86 }
87
88 /**
89 * List all the known authors of stories.
90 *
91 * @return the authors
92 */
93 public synchronized List<String> getAuthors() {
94 List<String> list = new ArrayList<String>();
95 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
96 String storyAuthor = entry.getKey().getAuthor();
97 if (!list.contains(storyAuthor)) {
98 list.add(storyAuthor);
99 }
100 }
101
102 Collections.sort(list);
103 return list;
104 }
105
106 /**
107 * List all the stories of the given author in the {@link Library}, or all
108 * the stories if NULL is passed as an author.
109 *
110 * @param author
111 * the author of the stories to retrieve, or NULL for all
112 *
113 * @return the stories
114 */
115 public synchronized List<MetaData> getListByAuthor(String author) {
116 List<MetaData> list = new ArrayList<MetaData>();
117 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
118 String storyAuthor = entry.getKey().getAuthor();
119 if (author == null || author.equalsIgnoreCase(storyAuthor)) {
120 list.add(entry.getKey());
121 }
122 }
123
124 Collections.sort(list);
125 return list;
126 }
127
128 /**
129 * List all the stories of the given source type in the {@link Library}, or
130 * all the stories if NULL is passed as a type.
131 *
132 * @param type
133 * the type of story to retrieve, or NULL for all
134 *
135 * @return the stories
136 */
137 public synchronized List<MetaData> getListByType(String type) {
138 List<MetaData> list = new ArrayList<MetaData>();
139 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
140 String storyType = entry.getValue().getParentFile().getName();
141 if (type == null || type.equalsIgnoreCase(storyType)) {
142 list.add(entry.getKey());
143 }
144 }
145
146 Collections.sort(list);
147 return list;
148 }
149
150 /**
151 * Retrieve a {@link File} corresponding to the given {@link Story}.
152 *
153 * @param luid
154 * the Library UID of the story
155 *
156 * @return the corresponding {@link Story}
157 */
158 public synchronized MetaData getInfo(String luid) {
159 if (luid != null) {
160 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
161 if (luid.equals(entry.getKey().getLuid())) {
162 return entry.getKey();
163 }
164 }
165 }
166
167 return null;
168 }
169
170 /**
171 * Retrieve a {@link File} corresponding to the given {@link Story}.
172 *
173 * @param luid
174 * the Library UID of the story
175 *
176 * @return the corresponding {@link Story}
177 */
178 public synchronized File getFile(String luid) {
179 if (luid != null) {
180 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
181 if (luid.equals(entry.getKey().getLuid())) {
182 return entry.getValue();
183 }
184 }
185 }
186
187 return null;
188 }
189
190 /**
191 * Retrieve a specific {@link Story}.
192 *
193 * @param luid
194 * the Library UID of the story
195 * @param pg
196 * the optional progress reporter
197 *
198 * @return the corresponding {@link Story} or NULL if not found
199 */
200 public synchronized Story getStory(String luid, Progress pg) {
201 if (luid != null) {
202 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
203 if (luid.equals(entry.getKey().getLuid())) {
204 try {
205 SupportType type = SupportType.valueOfAllOkUC(entry
206 .getKey().getType());
207 URL url = entry.getValue().toURI().toURL();
208 if (type != null) {
209 return BasicSupport.getSupport(type).process(url,
210 pg);
211 } else {
212 throw new IOException("Unknown type: "
213 + entry.getKey().getType());
214 }
215 } catch (IOException e) {
216 // We should not have not-supported files in the
217 // library
218 Instance.syserr(new IOException(
219 "Cannot load file from library: "
220 + entry.getValue().getPath(), e));
221 }
222 }
223 }
224 }
225
226 if (pg != null) {
227 pg.setMinMax(0, 1);
228 pg.setProgress(1);
229 }
230
231 return null;
232 }
233
234 /**
235 * Import the {@link Story} at the given {@link URL} into the
236 * {@link Library}.
237 *
238 * @param url
239 * the {@link URL} to import
240 * @param pg
241 * the optional progress reporter
242 *
243 * @return the imported {@link Story}
244 *
245 * @throws IOException
246 * in case of I/O error
247 */
248 public Story imprt(URL url, Progress pg) throws IOException {
249 BasicSupport support = BasicSupport.getSupport(url);
250 if (support == null) {
251 throw new IOException("URL not supported: " + url.toString());
252 }
253
254 return save(support.process(url, pg), null);
255 }
256
257 /**
258 * Export the {@link Story} to the given target in the given format.
259 *
260 * @param luid
261 * the {@link Story} ID
262 * @param type
263 * the {@link OutputType} to transform it to
264 * @param target
265 * the target to save to
266 * @param pg
267 * the optional progress reporter
268 *
269 * @return the saved resource (the main saved {@link File})
270 *
271 * @throws IOException
272 * in case of I/O error
273 */
274 public File export(String luid, OutputType type, String target, Progress pg)
275 throws IOException {
276 Progress pgGetStory = new Progress();
277 Progress pgOut = new Progress();
278 if (pg != null) {
279 pg.setMax(2);
280 pg.addProgress(pgGetStory, 1);
281 pg.addProgress(pgOut, 1);
282 }
283
284 BasicOutput out = BasicOutput.getOutput(type, true);
285 if (out == null) {
286 throw new IOException("Output type not supported: " + type);
287 }
288
289 Story story = getStory(luid, pgGetStory);
290 if (story == null) {
291 throw new IOException("Cannot find story to export: " + luid);
292 }
293
294 return out.process(story, target, pgOut);
295 }
296
297 /**
298 * Save a {@link Story} to the {@link Library}.
299 *
300 * @param story
301 * the {@link Story} to save
302 * @param pg
303 * the optional progress reporter
304 *
305 * @return the same {@link Story}, whose LUID may have changed
306 *
307 * @throws IOException
308 * in case of I/O error
309 */
310 public Story save(Story story, Progress pg) throws IOException {
311 return save(story, null, pg);
312 }
313
314 /**
315 * Save a {@link Story} to the {@link Library} -- the LUID <b>must</b> be
316 * correct, or NULL to get the next free one.
317 *
318 * @param story
319 * the {@link Story} to save
320 * @param luid
321 * the <b>correct</b> LUID or NULL to get the next free one
322 * @param pg
323 * the optional progress reporter
324 *
325 * @return the same {@link Story}, whose LUID may have changed
326 *
327 * @throws IOException
328 * in case of I/O error
329 */
330 public synchronized Story save(Story story, String luid, Progress pg)
331 throws IOException {
332 // Do not change the original metadata, but change the original story
333 MetaData key = story.getMeta().clone();
334 story.setMeta(key);
335
336 if (luid == null || luid.isEmpty()) {
337 getStories(null); // refresh lastId if needed
338 key.setLuid(String.format("%03d", (++lastId)));
339 } else {
340 key.setLuid(luid);
341 }
342
343 getDir(key).mkdirs();
344 if (!getDir(key).exists()) {
345 throw new IOException("Cannot create library dir");
346 }
347
348 OutputType out;
349 if (key != null && key.isImageDocument()) {
350 out = image;
351 } else {
352 out = text;
353 }
354
355 BasicOutput it = BasicOutput.getOutput(out, true);
356 it.process(story, getFile(key).getPath(), pg);
357
358 // empty cache
359 stories.clear();
360
361 return story;
362 }
363
364 /**
365 * Delete the given {@link Story} from this {@link Library}.
366 *
367 * @param luid
368 * the LUID of the target {@link Story}
369 *
370 * @return TRUE if it was deleted
371 */
372 public synchronized boolean delete(String luid) {
373 boolean ok = false;
374
375 MetaData meta = getInfo(luid);
376 File file = getStories(null).get(meta);
377
378 if (file != null) {
379 if (file.delete()) {
380 String readerExt = getOutputType(meta)
381 .getDefaultExtension(true);
382 String fileExt = getOutputType(meta).getDefaultExtension(false);
383
384 String path = file.getAbsolutePath();
385 if (readerExt != null && !readerExt.equals(fileExt)) {
386 path = path
387 .substring(0, path.length() - readerExt.length())
388 + fileExt;
389 file = new File(path);
390 IOUtils.deltree(file);
391 }
392
393 File infoFile = new File(path + ".info");
394 if (!infoFile.exists()) {
395 infoFile = new File(path.substring(0, path.length()
396 - fileExt.length())
397 + ".info");
398 }
399 infoFile.delete();
400
401 String coverExt = "."
402 + Instance.getConfig().getString(
403 Config.IMAGE_FORMAT_COVER);
404 File coverFile = new File(path + coverExt);
405 if (!coverFile.exists()) {
406 coverFile = new File(path.substring(0, path.length()
407 - fileExt.length()));
408 }
409 coverFile.delete();
410
411 ok = true;
412 }
413
414 // clear cache
415 stories.clear();
416 }
417
418 return ok;
419 }
420
421 /**
422 * The directory (full path) where the {@link Story} related to this
423 * {@link MetaData} should be located on disk.
424 *
425 * @param key
426 * the {@link Story} {@link MetaData}
427 *
428 * @return the target directory
429 */
430 private File getDir(MetaData key) {
431 String source = key.getSource().replaceAll("[^a-zA-Z0-9._+-]", "_");
432 return new File(baseDir, source);
433 }
434
435 /**
436 * The target (full path) where the {@link Story} related to this
437 * {@link MetaData} should be located on disk.
438 *
439 * @param key
440 * the {@link Story} {@link MetaData}
441 *
442 * @return the target
443 */
444 private File getFile(MetaData key) {
445 String title = key.getTitle();
446 if (title == null) {
447 title = "";
448 }
449 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
450 return new File(getDir(key), key.getLuid() + "_" + title);
451 }
452
453 /**
454 * Return all the known stories in this {@link Library} object.
455 *
456 * @param pg
457 * the optional progress reporter
458 *
459 * @return the stories
460 */
461 private synchronized Map<MetaData, File> getStories(Progress pg) {
462 if (pg == null) {
463 pg = new Progress();
464 } else {
465 pg.setMinMax(0, 100);
466 }
467
468 if (stories.isEmpty()) {
469 lastId = 0;
470
471 File[] dirs = baseDir.listFiles(new FileFilter() {
472 public boolean accept(File file) {
473 return file != null && file.isDirectory();
474 }
475 });
476
477 Progress pgDirs = new Progress(0, 100 * dirs.length);
478 pg.addProgress(pgDirs, 100);
479
480 final String ext = ".info";
481 for (File dir : dirs) {
482 File[] files = dir.listFiles(new FileFilter() {
483 public boolean accept(File file) {
484 return file != null
485 && file.getPath().toLowerCase().endsWith(ext);
486 }
487 });
488
489 Progress pgFiles = new Progress(0, files.length);
490 pgDirs.addProgress(pgFiles, 100);
491 pgDirs.setName("Loading from: " + dir.getName());
492
493 for (File file : files) {
494 try {
495 pgFiles.setName(file.getName());
496 MetaData meta = InfoReader.readMeta(file);
497 try {
498 int id = Integer.parseInt(meta.getLuid());
499 if (id > lastId) {
500 lastId = id;
501 }
502
503 // Replace .info with whatever is needed:
504 String path = file.getPath();
505 path = path.substring(0,
506 path.length() - ext.length());
507
508 String newExt = getOutputType(meta)
509 .getDefaultExtension(true);
510
511 file = new File(path + newExt);
512 //
513
514 stories.put(meta, file);
515
516 } catch (Exception e) {
517 // not normal!!
518 Instance.syserr(new IOException(
519 "Cannot understand the LUID of "
520 + file.getPath() + ": "
521 + meta.getLuid(), e));
522 }
523 } catch (IOException e) {
524 // We should not have not-supported files in the
525 // library
526 Instance.syserr(new IOException(
527 "Cannot load file from library: "
528 + file.getPath(), e));
529 } finally {
530 pgFiles.add(1);
531 }
532 }
533
534 pgFiles.setName(null);
535 }
536
537 pgDirs.setName("Loading directories");
538 }
539
540 return stories;
541 }
542
543 /**
544 * Return the {@link OutputType} for this {@link Story}.
545 *
546 * @param meta
547 * the {@link Story} {@link MetaData}
548 *
549 * @return the type
550 */
551 private OutputType getOutputType(MetaData meta) {
552 if (meta != null && meta.isImageDocument()) {
553 return image;
554 } else {
555 return text;
556 }
557 }
558 }