Version 1.4.2
[fanfix.git] / src / be / nikiroo / fanfix / Library.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix;
2
3import java.io.File;
4f661b2b 4import java.io.FileFilter;
08fe2e33
NR
5import java.io.IOException;
6import java.net.URL;
7import java.util.ArrayList;
22848428 8import java.util.Collections;
08fe2e33
NR
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12import java.util.Map.Entry;
13
10d558d2 14import be.nikiroo.fanfix.bundles.Config;
08fe2e33
NR
15import be.nikiroo.fanfix.data.MetaData;
16import be.nikiroo.fanfix.data.Story;
17import be.nikiroo.fanfix.output.BasicOutput;
18import be.nikiroo.fanfix.output.BasicOutput.OutputType;
19import be.nikiroo.fanfix.supported.BasicSupport;
20import be.nikiroo.fanfix.supported.BasicSupport.SupportType;
68686a37 21import be.nikiroo.fanfix.supported.InfoReader;
9843a5e5 22import be.nikiroo.utils.IOUtils;
3b2b638f 23import be.nikiroo.utils.Progress;
08fe2e33
NR
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 */
33public class Library {
34 private File baseDir;
35 private Map<MetaData, File> stories;
08fe2e33 36 private int lastId;
2206ef66
NR
37 private OutputType text;
38 private OutputType image;
08fe2e33
NR
39
40 /**
41 * Create a new {@link Library} with the given backend directory.
42 *
43 * @param dir
2206ef66
NR
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
08fe2e33 49 */
2206ef66 50 public Library(File dir, OutputType text, OutputType image) {
08fe2e33
NR
51 this.baseDir = dir;
52 this.stories = new HashMap<MetaData, File>();
53 this.lastId = 0;
2206ef66
NR
54 this.text = text;
55 this.image = image;
08fe2e33
NR
56
57 dir.mkdirs();
58 }
59
4f661b2b
NR
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
333f0e7b
NR
70 /**
71 * List all the known types of stories.
72 *
73 * @return the types
74 */
10d558d2 75 public synchronized List<String> getTypes() {
333f0e7b 76 List<String> list = new ArrayList<String>();
4f661b2b 77 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9 78 String storyType = entry.getKey().getSource();
333f0e7b
NR
79 if (!list.contains(storyType)) {
80 list.add(storyType);
81 }
82 }
83
4310bae9
NR
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>();
4f661b2b 95 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
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>();
4f661b2b 117 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
4310bae9
NR
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);
333f0e7b
NR
125 return list;
126 }
127
08fe2e33
NR
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 */
4310bae9 137 public synchronized List<MetaData> getListByType(String type) {
08fe2e33 138 List<MetaData> list = new ArrayList<MetaData>();
4f661b2b 139 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33 140 String storyType = entry.getValue().getParentFile().getName();
333f0e7b 141 if (type == null || type.equalsIgnoreCase(storyType)) {
08fe2e33
NR
142 list.add(entry.getKey());
143 }
144 }
145
22848428 146 Collections.sort(list);
08fe2e33
NR
147 return list;
148 }
149
301791d3
NR
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 */
10d558d2 158 public synchronized MetaData getInfo(String luid) {
301791d3 159 if (luid != null) {
4f661b2b 160 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
301791d3
NR
161 if (luid.equals(entry.getKey().getLuid())) {
162 return entry.getKey();
163 }
164 }
165 }
166
167 return null;
168 }
169
2206ef66
NR
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 */
10d558d2 178 public synchronized File getFile(String luid) {
2206ef66 179 if (luid != null) {
4f661b2b 180 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
2206ef66
NR
181 if (luid.equals(entry.getKey().getLuid())) {
182 return entry.getValue();
183 }
184 }
185 }
186
187 return null;
188 }
189
08fe2e33
NR
190 /**
191 * Retrieve a specific {@link Story}.
192 *
193 * @param luid
194 * the Library UID of the story
92fb0719
NR
195 * @param pg
196 * the optional progress reporter
08fe2e33 197 *
3d247bc3 198 * @return the corresponding {@link Story} or NULL if not found
08fe2e33 199 */
10d558d2 200 public synchronized Story getStory(String luid, Progress pg) {
08fe2e33 201 if (luid != null) {
4f661b2b 202 for (Entry<MetaData, File> entry : getStories(null).entrySet()) {
08fe2e33
NR
203 if (luid.equals(entry.getKey().getLuid())) {
204 try {
fe999aa4
NR
205 SupportType type = SupportType.valueOfAllOkUC(entry
206 .getKey().getType());
207 URL url = entry.getValue().toURI().toURL();
208 if (type != null) {
92fb0719
NR
209 return BasicSupport.getSupport(type).process(url,
210 pg);
fe999aa4
NR
211 } else {
212 throw new IOException("Unknown type: "
213 + entry.getKey().getType());
214 }
08fe2e33
NR
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
92fb0719
NR
226 if (pg != null) {
227 pg.setMinMax(0, 1);
228 pg.setProgress(1);
229 }
230
08fe2e33
NR
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
92fb0719
NR
240 * @param pg
241 * the optional progress reporter
08fe2e33
NR
242 *
243 * @return the imported {@link Story}
244 *
245 * @throws IOException
246 * in case of I/O error
247 */
92fb0719 248 public Story imprt(URL url, Progress pg) throws IOException {
08fe2e33
NR
249 BasicSupport support = BasicSupport.getSupport(url);
250 if (support == null) {
251 throw new IOException("URL not supported: " + url.toString());
252 }
253
92fb0719 254 return save(support.process(url, pg), null);
08fe2e33
NR
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
92fb0719
NR
266 * @param pg
267 * the optional progress reporter
08fe2e33
NR
268 *
269 * @return the saved resource (the main saved {@link File})
270 *
271 * @throws IOException
272 * in case of I/O error
273 */
92fb0719 274 public File export(String luid, OutputType type, String target, Progress pg)
08fe2e33 275 throws IOException {
bee7dffe
NR
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
08fe2e33
NR
284 BasicOutput out = BasicOutput.getOutput(type, true);
285 if (out == null) {
286 throw new IOException("Output type not supported: " + type);
287 }
288
bee7dffe 289 Story story = getStory(luid, pgGetStory);
73ce17ef
NR
290 if (story == null) {
291 throw new IOException("Cannot find story to export: " + luid);
292 }
293
bee7dffe 294 return out.process(story, target, pgOut);
08fe2e33
NR
295 }
296
297 /**
2206ef66
NR
298 * Save a {@link Story} to the {@link Library}.
299 *
300 * @param story
301 * the {@link Story} to save
bee7dffe
NR
302 * @param pg
303 * the optional progress reporter
2206ef66
NR
304 *
305 * @return the same {@link Story}, whose LUID may have changed
306 *
307 * @throws IOException
308 * in case of I/O error
309 */
bee7dffe
NR
310 public Story save(Story story, Progress pg) throws IOException {
311 return save(story, null, pg);
2206ef66
NR
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.
08fe2e33
NR
317 *
318 * @param story
319 * the {@link Story} to save
2206ef66
NR
320 * @param luid
321 * the <b>correct</b> LUID or NULL to get the next free one
bee7dffe
NR
322 * @param pg
323 * the optional progress reporter
2206ef66
NR
324 *
325 * @return the same {@link Story}, whose LUID may have changed
08fe2e33
NR
326 *
327 * @throws IOException
328 * in case of I/O error
329 */
10d558d2
NR
330 public synchronized Story save(Story story, String luid, Progress pg)
331 throws IOException {
a7d266e6 332 // Do not change the original metadata, but change the original story
301791d3 333 MetaData key = story.getMeta().clone();
a7d266e6 334 story.setMeta(key);
08fe2e33 335
2206ef66 336 if (luid == null || luid.isEmpty()) {
4f661b2b 337 getStories(null); // refresh lastId if needed
2206ef66
NR
338 key.setLuid(String.format("%03d", (++lastId)));
339 } else {
340 key.setLuid(luid);
341 }
342
08fe2e33
NR
343 getDir(key).mkdirs();
344 if (!getDir(key).exists()) {
345 throw new IOException("Cannot create library dir");
346 }
347
348 OutputType out;
08fe2e33 349 if (key != null && key.isImageDocument()) {
2206ef66 350 out = image;
08fe2e33 351 } else {
2206ef66 352 out = text;
08fe2e33 353 }
2206ef66 354
08fe2e33 355 BasicOutput it = BasicOutput.getOutput(out, true);
10d558d2
NR
356 it.process(story, getFile(key).getPath(), pg);
357
358 // empty cache
359 stories.clear();
2206ef66
NR
360
361 return story;
08fe2e33
NR
362 }
363
10d558d2
NR
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);
4f661b2b 376 File file = getStories(null).get(meta);
10d558d2
NR
377
378 if (file != null) {
379 if (file.delete()) {
9843a5e5
NR
380 String readerExt = getOutputType(meta)
381 .getDefaultExtension(true);
382 String fileExt = getOutputType(meta).getDefaultExtension(false);
10d558d2
NR
383
384 String path = file.getAbsolutePath();
9843a5e5
NR
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
10d558d2
NR
393 File infoFile = new File(path + ".info");
394 if (!infoFile.exists()) {
395 infoFile = new File(path.substring(0, path.length()
9843a5e5 396 - fileExt.length())
10d558d2
NR
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()
9843a5e5 407 - fileExt.length()));
10d558d2
NR
408 }
409 coverFile.delete();
410
411 ok = true;
412 }
413
414 // clear cache
415 stories.clear();
416 }
417
418 return ok;
419 }
420
08fe2e33
NR
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) {
a4143cd7
NR
445 String title = key.getTitle();
446 if (title == null) {
447 title = "";
448 }
449 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
08fe2e33
NR
450 return new File(getDir(key), key.getLuid() + "_" + title);
451 }
452
453 /**
454 * Return all the known stories in this {@link Library} object.
455 *
4f661b2b
NR
456 * @param pg
457 * the optional progress reporter
458 *
08fe2e33
NR
459 * @return the stories
460 */
4f661b2b
NR
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
08fe2e33
NR
468 if (stories.isEmpty()) {
469 lastId = 0;
68686a37 470
4f661b2b
NR
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);
08fe2e33 497 try {
4f661b2b
NR
498 int id = Integer.parseInt(meta.getLuid());
499 if (id > lastId) {
500 lastId = id;
08fe2e33 501 }
4f661b2b
NR
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!!
08fe2e33 518 Instance.syserr(new IOException(
4f661b2b
NR
519 "Cannot understand the LUID of "
520 + file.getPath() + ": "
521 + meta.getLuid(), e));
08fe2e33 522 }
4f661b2b
NR
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 {
5ce869b8 530 pgFiles.add(1);
08fe2e33
NR
531 }
532 }
4f661b2b
NR
533
534 pgFiles.setName(null);
08fe2e33 535 }
4f661b2b
NR
536
537 pgDirs.setName("Loading directories");
08fe2e33
NR
538 }
539
540 return stories;
541 }
2206ef66
NR
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 }
08fe2e33 558}