Dependency fix + Local/Remote Library support
[fanfix.git] / src / be / nikiroo / fanfix / LocalLibrary.java
1 package be.nikiroo.fanfix;
2
3 import java.awt.image.BufferedImage;
4 import java.io.File;
5 import java.io.FileFilter;
6 import java.io.IOException;
7 import java.util.ArrayList;
8 import java.util.HashMap;
9 import java.util.List;
10 import java.util.Map;
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.output.InfoCover;
18 import be.nikiroo.fanfix.supported.InfoReader;
19 import be.nikiroo.utils.IOUtils;
20 import be.nikiroo.utils.Progress;
21
22 /**
23 * This {@link BasicLibrary} will store the stories locally on disk.
24 *
25 * @author niki
26 */
27 public class LocalLibrary extends BasicLibrary {
28 private int lastId;
29 private Map<MetaData, File[]> stories; // Files: [ infoFile, TargetFile ]
30
31 private File baseDir;
32 private OutputType text;
33 private OutputType image;
34
35 /**
36 * Create a new {@link LocalLibrary} with the given back-end directory.
37 *
38 * @param baseDir
39 * the directory where to find the {@link Story} objects
40 * @param text
41 * the {@link OutputType} to save the text-focused stories into
42 * @param image
43 * the {@link OutputType} to save the images-focused stories into
44 */
45 public LocalLibrary(File baseDir, OutputType text, OutputType image) {
46 this.baseDir = baseDir;
47 this.text = text;
48 this.image = image;
49
50 this.lastId = 0;
51 this.stories = null;
52
53 baseDir.mkdirs();
54 }
55
56 @Override
57 protected List<MetaData> getMetas(Progress pg) {
58 return new ArrayList<MetaData>(getStories(pg).keySet());
59 }
60
61 @Override
62 public File getFile(String luid) {
63 File[] files = getStories(null).get(getInfo(luid));
64 if (files != null) {
65 return files[1];
66 }
67
68 return null;
69 }
70
71 @Override
72 public BufferedImage getCover(String luid) {
73 MetaData meta = getInfo(luid);
74 if (meta != null) {
75 File[] files = getStories(null).get(meta);
76 if (files != null) {
77 File infoFile = files[0];
78
79 try {
80 meta = InfoReader.readMeta(infoFile, true);
81 return meta.getCover();
82 } catch (IOException e) {
83 Instance.syserr(e);
84 }
85 }
86 }
87
88 return null;
89 }
90
91 @Override
92 protected void clearCache() {
93 stories = null;
94 }
95
96 @Override
97 protected synchronized int getNextId() {
98 return ++lastId;
99 }
100
101 @Override
102 protected void doDelete(String luid) throws IOException {
103 for (File file : getRelatedFiles(luid)) {
104 // TODO: throw an IOException if we cannot delete the files?
105 IOUtils.deltree(file);
106 }
107 }
108
109 @Override
110 protected Story doSave(Story story, Progress pg) throws IOException {
111 MetaData meta = story.getMeta();
112
113 File expectedTarget = getExpectedFile(meta);
114 expectedTarget.getParentFile().mkdirs();
115
116 BasicOutput it = BasicOutput.getOutput(getOutputType(meta), true);
117 it.process(story, expectedTarget.getPath(), pg);
118
119 return story;
120 }
121
122 @Override
123 protected synchronized void saveMeta(MetaData meta, Progress pg)
124 throws IOException {
125 File newDir = getExpectedDir(meta.getSource());
126 if (!newDir.exists()) {
127 newDir.mkdir();
128 }
129
130 List<File> relatedFiles = getRelatedFiles(meta.getLuid());
131 for (File relatedFile : relatedFiles) {
132 // TODO: this is not safe at all.
133 // We should copy all the files THEN delete them
134 // Maybe also adding some rollback cleanup if possible
135 if (relatedFile.getName().endsWith(".info")) {
136 try {
137 String name = relatedFile.getName().replaceFirst(
138 "\\.info$", "");
139 InfoCover.writeInfo(newDir, name, meta);
140 relatedFile.delete();
141 } catch (IOException e) {
142 Instance.syserr(e);
143 }
144 } else {
145 relatedFile.renameTo(new File(newDir, relatedFile.getName()));
146 }
147 }
148
149 clearCache();
150 }
151
152 /**
153 * Return the {@link OutputType} for this {@link Story}.
154 *
155 * @param meta
156 * the {@link Story} {@link MetaData}
157 *
158 * @return the type
159 */
160 private OutputType getOutputType(MetaData meta) {
161 if (meta != null && meta.isImageDocument()) {
162 return image;
163 } else {
164 return text;
165 }
166 }
167
168 /**
169 * Get the target {@link File} related to the given <tt>.info</tt>
170 * {@link File} and {@link MetaData}.
171 *
172 * @param meta
173 * the meta
174 * @param infoFile
175 * the <tt>.info</tt> {@link File}
176 *
177 * @return the target {@link File}
178 */
179 private File getTargetFile(MetaData meta, File infoFile) {
180 // Replace .info with whatever is needed:
181 String path = infoFile.getPath();
182 path = path.substring(0, path.length() - ".info".length());
183 String newExt = getOutputType(meta).getDefaultExtension(true);
184
185 return new File(path + newExt);
186 }
187
188 /**
189 * The target (full path) where the {@link Story} related to this
190 * {@link MetaData} should be located on disk for a new {@link Story}.
191 *
192 * @param key
193 * the {@link Story} {@link MetaData}
194 *
195 * @return the target
196 */
197 private File getExpectedFile(MetaData key) {
198 String title = key.getTitle();
199 if (title == null) {
200 title = "";
201 }
202 title = title.replaceAll("[^a-zA-Z0-9._+-]", "_");
203 return new File(getExpectedDir(key.getSource()), key.getLuid() + "_"
204 + title);
205 }
206
207 /**
208 * The directory (full path) where the new {@link Story} related to this
209 * {@link MetaData} should be located on disk.
210 *
211 * @param type
212 * the type (source)
213 *
214 * @return the target directory
215 */
216 private File getExpectedDir(String type) {
217 String source = type.replaceAll("[^a-zA-Z0-9._+-]", "_");
218 return new File(baseDir, source);
219 }
220
221 /**
222 * Return the list of files/directories on disk for this {@link Story}.
223 * <p>
224 * If the {@link Story} is not found, and empty list is returned.
225 *
226 * @param luid
227 * the {@link Story} LUID
228 *
229 * @return the list of {@link File}s
230 *
231 * @throws IOException
232 * if the {@link Story} was not found
233 */
234 private List<File> getRelatedFiles(String luid) throws IOException {
235 List<File> files = new ArrayList<File>();
236
237 MetaData meta = getInfo(luid);
238 if (meta == null) {
239 throw new IOException("Story not found: " + luid);
240 } else {
241 File infoFile = getStories(null).get(meta)[0];
242 File targetFile = getStories(null).get(meta)[1];
243
244 files.add(infoFile);
245 files.add(targetFile);
246
247 String readerExt = getOutputType(meta).getDefaultExtension(true);
248 String fileExt = getOutputType(meta).getDefaultExtension(false);
249
250 String path = targetFile.getAbsolutePath();
251 if (readerExt != null && !readerExt.equals(fileExt)) {
252 path = path.substring(0, path.length() - readerExt.length())
253 + fileExt;
254 File relatedFile = new File(path);
255
256 if (relatedFile.exists()) {
257 files.add(relatedFile);
258 }
259 }
260
261 String coverExt = "."
262 + Instance.getConfig().getString(Config.IMAGE_FORMAT_COVER);
263 File coverFile = new File(path + coverExt);
264 if (!coverFile.exists()) {
265 coverFile = new File(path.substring(0,
266 path.length() - fileExt.length())
267 + coverExt);
268 }
269
270 if (coverFile.exists()) {
271 files.add(coverFile);
272 }
273 }
274
275 return files;
276 }
277
278 /**
279 * Fill the list of stories by reading the content of the local directory
280 * {@link LocalLibrary#baseDir}.
281 * <p>
282 * Will use a cached list when possible (see
283 * {@link BasicLibrary#clearCache()}).
284 *
285 * @param pg
286 * the optional {@link Progress}
287 *
288 * @return the list of stories
289 */
290 private synchronized Map<MetaData, File[]> getStories(Progress pg) {
291 if (pg == null) {
292 pg = new Progress();
293 } else {
294 pg.setMinMax(0, 100);
295 }
296
297 if (stories == null) {
298 stories = new HashMap<MetaData, File[]>();
299
300 lastId = 0;
301
302 File[] dirs = baseDir.listFiles(new FileFilter() {
303 public boolean accept(File file) {
304 return file != null && file.isDirectory();
305 }
306 });
307
308 Progress pgDirs = new Progress(0, 100 * dirs.length);
309 pg.addProgress(pgDirs, 100);
310
311 for (File dir : dirs) {
312 File[] infoFiles = dir.listFiles(new FileFilter() {
313 public boolean accept(File file) {
314 return file != null
315 && file.getPath().toLowerCase()
316 .endsWith(".info");
317 }
318 });
319
320 Progress pgFiles = new Progress(0, infoFiles.length);
321 pgDirs.addProgress(pgFiles, 100);
322 pgDirs.setName("Loading from: " + dir.getName());
323
324 for (File infoFile : infoFiles) {
325 pgFiles.setName(infoFile.getName());
326 try {
327 MetaData meta = InfoReader.readMeta(infoFile, false);
328 try {
329 int id = Integer.parseInt(meta.getLuid());
330 if (id > lastId) {
331 lastId = id;
332 }
333
334 stories.put(meta, new File[] { infoFile,
335 getTargetFile(meta, infoFile) });
336 } catch (Exception e) {
337 // not normal!!
338 throw new IOException(
339 "Cannot understand the LUID of "
340 + infoFile
341 + ": "
342 + (meta == null ? "[meta is NULL]"
343 : meta.getLuid()), e);
344 }
345 } catch (IOException e) {
346 // We should not have not-supported files in the
347 // library
348 Instance.syserr(new IOException(
349 "Cannot load file from library: " + infoFile, e));
350 }
351 pgFiles.add(1);
352 }
353
354 pgFiles.setName(null);
355 }
356
357 pgDirs.setName("Loading directories");
358 }
359
360 return stories;
361 }
362 }