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