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