Update nikiroo-utils (Progress) + GuiReader perf
[fanfix.git] / src / be / nikiroo / fanfix / library / LocalLibrary.java
1 package be.nikiroo.fanfix.library;
2
3 import java.awt.image.BufferedImage;
4 import java.io.File;
5 import java.io.FileFilter;
6 import java.io.FileInputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.util.ArrayList;
10 import java.util.HashMap;
11 import java.util.List;
12 import java.util.Map;
13
14 import javax.imageio.ImageIO;
15
16 import be.nikiroo.fanfix.Instance;
17 import be.nikiroo.fanfix.bundles.Config;
18 import be.nikiroo.fanfix.data.MetaData;
19 import be.nikiroo.fanfix.data.Story;
20 import be.nikiroo.fanfix.output.BasicOutput;
21 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
22 import be.nikiroo.fanfix.output.InfoCover;
23 import be.nikiroo.fanfix.supported.InfoReader;
24 import be.nikiroo.utils.IOUtils;
25 import be.nikiroo.utils.ImageUtils;
26 import be.nikiroo.utils.MarkableFileInputStream;
27 import be.nikiroo.utils.Progress;
28
29 /**
30 * This {@link BasicLibrary} will store the stories locally on disk.
31 *
32 * @author niki
33 */
34 public class LocalLibrary extends BasicLibrary {
35 private int lastId;
36 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
37 private Map<String, BufferedImage> sourceCovers;
38
39 private File baseDir;
40 private OutputType text;
41 private OutputType image;
42
43 /**
44 * Create a new {@link LocalLibrary} with the given back-end directory.
45 *
46 * @param baseDir
47 * the directory where to find the {@link Story} objects
48 * @param text
49 * the {@link OutputType} to save the text-focused stories into
50 * @param image
51 * the {@link OutputType} to save the images-focused stories into
52 */
53 public LocalLibrary(File baseDir, OutputType text, OutputType image) {
54 this.baseDir = baseDir;
55 this.text = text;
56 this.image = image;
57
58 this.lastId = 0;
59 this.stories = null;
60 this.sourceCovers = new HashMap<String, BufferedImage>();
61
62 baseDir.mkdirs();
63 }
64
65 @Override
66 protected List<MetaData> getMetas(Progress pg) {
67 return new ArrayList<MetaData>(getStories(pg).keySet());
68 }
69
70 @Override
71 public File getFile(String luid) {
72 File[] files = getStories(null).get(getInfo(luid));
73 if (files != null) {
74 return files[1];
75 }
76
77 return null;
78 }
79
80 @Override
81 public BufferedImage getCover(String luid) {
82 MetaData meta = getInfo(luid);
83 if (meta != null) {
84 File[] files = getStories(null).get(meta);
85 if (files != null) {
86 File infoFile = files[0];
87
88 try {
89 meta = InfoReader.readMeta(infoFile, true);
90 return meta.getCover();
91 } catch (IOException e) {
92 Instance.syserr(e);
93 }
94 }
95 }
96
97 return null;
98 }
99
100 @Override
101 protected void clearCache() {
102 stories = null;
103 sourceCovers = new HashMap<String, BufferedImage>();
104 }
105
106 @Override
107 protected synchronized int getNextId() {
108 getStories(null); // make sure lastId is set
109 return ++lastId;
110 }
111
112 @Override
113 protected void doDelete(String luid) throws IOException {
114 for (File file : getRelatedFiles(luid)) {
115 // TODO: throw an IOException if we cannot delete the files?
116 IOUtils.deltree(file);
117 }
118 }
119
120 @Override
121 protected Story doSave(Story story, Progress pg) throws IOException {
122 MetaData meta = story.getMeta();
123
124 File expectedTarget = getExpectedFile(meta);
125 expectedTarget.getParentFile().mkdirs();
126
127 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true);
128 it.process(story, expectedTarget.getPath(), pg);
129
130 return story;
131 }
132
133 @Override
134 protected synchronized void saveMeta(MetaData meta, Progress pg)
135 throws IOException {
136 File newDir = getExpectedDir(meta.getSource());
137 if (!newDir.exists()) {
138 newDir.mkdir();
139 }
140
141 List<File> relatedFiles = getRelatedFiles(meta.getLuid());
142 for (File relatedFile : relatedFiles) {
143 // TODO: this is not safe at all.
144 // We should copy all the files THEN delete them
145 // Maybe also adding some rollback cleanup if possible
146 if (relatedFile.getName().endsWith(".info")) {
147 try {
148 String name = relatedFile.getName().replaceFirst(
149 "\\.info$", "");
150 InfoCover.writeInfo(newDir, name, meta);
151 relatedFile.delete();
152 } catch (IOException e) {
153 Instance.syserr(e);
154 }
155 } else {
156 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
157 }
158 }
159
160 clearCache();
161 }
162
163 @Override
164 public BufferedImage getSourceCover(String source) {
165 if (!sourceCovers.containsKey(source)) {
166 sourceCovers.put(source, super.getSourceCover(source));
167 }
168
169 return sourceCovers.get(source);
170 }
171
172 @Override
173 public void setSourceCover(String source, String luid) {
174 sourceCovers.put(source, getCover(luid));
175 File cover = new File(getExpectedDir(source), ".cover.png");
176 try {
177 ImageIO.write(sourceCovers.get(source), "png", cover);
178 } catch (IOException e) {
179 Instance.syserr(e);
180 sourceCovers.remove(source);
181 }
182 }
183
184 @Override
185 public void imprt(BasicLibrary other, String luid, Progress pg)
186 throws IOException {
187 if (pg == null) {
188 pg = new Progress();
189 }
190
191 // Check if we can simply copy the files instead of the whole process
192 if (other instanceof LocalLibrary) {
193 LocalLibrary otherLibrary = (LocalLibrary) other;
194 MetaData meta = otherLibrary.getInfo(luid);
195 String expectedType = "" + (meta.isImageDocument() ? image : text);
196 if (meta.getType().equals(expectedType)) {
197 File from = otherLibrary.getExpectedDir(meta.getSource());
198 File to = this.getExpectedDir(meta.getSource());
199 List<File> sources = otherLibrary.getRelatedFiles(luid);
200 if (!sources.isEmpty()) {
201 pg.setMinMax(0, sources.size());
202 }
203
204 for (File source : sources) {
205 File target = new File(source.getAbsolutePath().replace(
206 from.getAbsolutePath(), to.getAbsolutePath()));
207 if (!source.equals(target)) {
208 InputStream in = null;
209 try {
210 in = new FileInputStream(source);
211 IOUtils.write(in, target);
212 } catch (IOException e) {
213 if (in != null) {
214 try {
215 in.close();
216 } catch (Exception ee) {
217 }
218 }
219
220 pg.done();
221 throw e;
222 }
223 }
224
225 pg.add(1);
226 }
227
228 clearCache();
229 pg.done();
230 return;
231 }
232 }
233
234 super.imprt(other, luid, pg);
235 }
236
237 /**
238 * Return the {@link OutputType} for this {@link Story}.
239 *
240 * @param meta
241 * the {@link Story} {@link MetaData}
242 *
243 * @return the type
244 */
245 private OutputType getOutputType(MetaData meta) {
246 if (meta != null && meta.isImageDocument()) {
247 return image;
248 }
249
250 return text;
251 }
252
253 /**
254 * Get the target {@link File} related to the given <tt>.info</tt>
255 * {@link File} and {@link MetaData}.
256 *
257 * @param meta
258 * the meta
259 * @param infoFile
260 * the <tt>.info</tt> {@link File}
261 *
262 * @return the target {@link File}
263 */
264 private File getTargetFile(MetaData meta, File infoFile) {
265 // Replace .info with whatever is needed:
266 String path = infoFile.getPath();
267 path = path.substring(0, path.length() - ".info".length());
268 String newExt = getOutputType(meta).getDefaultExtension(true);
269
270 return new File(path + newExt);
271 }
272
273 /**
274 * The target (full path) where the {@link Story} related to this
275 * {@link MetaData} should be located on disk for a new {@link Story}.
276 *
277 * @param key
278 * the {@link Story} {@link MetaData}
279 *
280 * @return the target
281 */
282 private File getExpectedFile(MetaData key) {
283 String title = key.getTitle();
284 if (title == null) {
285 title = "";
286 }
287 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
288 return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
289 + title);
290 }
291
292 /**
293 * The directory (full path) where the new {@link Story} related to this
294 * {@link MetaData} should be located on disk.
295 *
296 * @param type
297 * the type (source)
298 *
299 * @return the target directory
300 */
301 private File getExpectedDir(String type) {
302 String source = type.replaceAll("[^a-zA-Z0-9._+-]", "_");
303 return new File(baseDir, source);
304 }
305
306 /**
307 * Return the list of files/directories on disk for this {@link Story}.
308 * <p>
309 * If the {@link Story} is not found, and empty list is returned.
310 *
311 * @param luid
312 * the {@link Story} LUID
313 *
314 * @return the list of {@link File}s
315 *
316 * @throws IOException
317 * if the {@link Story} was not found
318 */
319 private List<File> getRelatedFiles(String luid) throws IOException {
320 List<File> files = new ArrayList<File>();
321
322 MetaData meta = getInfo(luid);
323 if (meta == null) {
324 throw new IOException("Story not found: " + luid);
325 }
326
327 File infoFile = getStories(null).get(meta)[0];
328 File targetFile = getStories(null).get(meta)[1];
329
330 files.add(infoFile);
331 files.add(targetFile);
332
333 String readerExt = getOutputType(meta).getDefaultExtension(true);
334 String fileExt = getOutputType(meta).getDefaultExtension(false);
335
336 String path = targetFile.getAbsolutePath();
337 if (readerExt != null && !readerExt.equals(fileExt)) {
338 path = path.substring(0, path.length() - readerExt.length())
339 + fileExt;
340 File relatedFile = new File(path);
341
342 if (relatedFile.exists()) {
343 files.add(relatedFile);
344 }
345 }
346
347 String coverExt = "."
348 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER);
349 File coverFile = new File(path + coverExt);
350 if (!coverFile.exists()) {
351 coverFile = new File(path.substring(0,
352 path.length() - fileExt.length())
353 + coverExt);
354 }
355
356 if (coverFile.exists()) {
357 files.add(coverFile);
358 }
359
360 return files;
361 }
362
363 /**
364 * Fill the list of stories by reading the content of the local directory
365 * {@link LocalLibrary#baseDir}.
366 * <p>
367 * Will use a cached list when possible (see
368 * {@link BasicLibrary#clearCache()}).
369 *
370 * @param pg
371 * the optional {@link Progress}
372 *
373 * @return the list of stories
374 */
375 private synchronized Map<MetaData, File[]> getStories(Progress pg) {
376 if (pg == null) {
377 pg = new Progress();
378 } else {
379 pg.setMinMax(0, 100);
380 }
381
382 if (stories == null) {
383 stories = new HashMap<MetaData, File[]>();
384
385 lastId = 0;
386
387 File[] dirs = baseDir.listFiles(new FileFilter() {
388 @Override
389 public boolean accept(File file) {
390 return file != null && file.isDirectory();
391 }
392 });
393
394 Progress pgDirs = new Progress(0, 100 * dirs.length);
395 pg.addProgress(pgDirs, 100);
396
397 for (File dir : dirs) {
398 File[] infoFiles = dir.listFiles(new FileFilter() {
399 @Override
400 public boolean accept(File file) {
401 return file != null
402 && file.getPath().toLowerCase()
403 .endsWith(".info");
404 }
405 });
406
407 Progress pgFiles = new Progress(0, infoFiles.length);
408 pgDirs.addProgress(pgFiles, 100);
409 pgDirs.setName("Loading from: " + dir.getName());
410
411 String source = null;
412 for (File infoFile : infoFiles) {
413 pgFiles.setName(infoFile.getName());
414 try {
415 MetaData meta = InfoReader.readMeta(infoFile, false);
416 source = meta.getSource();
417 try {
418 int id = Integer.parseInt(meta.getLuid());
419 if (id > lastId) {
420 lastId = id;
421 }
422
423 stories.put(meta, new File[] { infoFile,
424 getTargetFile(meta, infoFile) });
425 } catch (Exception e) {
426 // not normal!!
427 throw new IOException(
428 "Cannot understand the LUID of "
429 + infoFile
430 + ": "
431 + (meta == null ? "[meta is NULL]"
432 : meta.getLuid()), e);
433 }
434 } catch (IOException e) {
435 // We should not have not-supported files in the
436 // library
437 Instance.syserr(new IOException(
438 "Cannot load file from library: " + infoFile, e));
439 }
440 pgFiles.add(1);
441 }
442
443 File cover = new File(dir, ".cover.png");
444 if (cover.exists()) {
445 try {
446 InputStream in = new MarkableFileInputStream(
447 new FileInputStream(cover));
448 try {
449 sourceCovers.put(source, ImageUtils.fromStream(in));
450 } finally {
451 in.close();
452 }
453 } catch (IOException e) {
454 Instance.syserr(e);
455 }
456 }
457
458 pgFiles.setName(null);
459 }
460
461 pgDirs.setName("Loading directories");
462 }
463
464 return stories;
465 }
466 }