Commit | Line | Data |
---|---|---|
08fe2e33 NR |
1 | package be.nikiroo.fanfix.supported; |
2 | ||
3 | import java.io.File; | |
4 | import java.io.IOException; | |
5 | import java.io.InputStream; | |
6 | import java.net.URISyntaxException; | |
7 | import java.net.URL; | |
7445f856 | 8 | import java.util.AbstractMap; |
08fe2e33 NR |
9 | import java.util.ArrayList; |
10 | import java.util.List; | |
11 | import java.util.Map.Entry; | |
12 | import java.util.Scanner; | |
13 | ||
7445f856 NR |
14 | import org.jsoup.nodes.Document; |
15 | ||
08fe2e33 NR |
16 | import be.nikiroo.fanfix.Instance; |
17 | import be.nikiroo.fanfix.bundles.Config; | |
68686a37 | 18 | import be.nikiroo.fanfix.data.MetaData; |
16a81ef7 | 19 | import be.nikiroo.utils.Image; |
81b5e730 | 20 | import be.nikiroo.utils.ImageUtils; |
ed08c171 | 21 | import be.nikiroo.utils.Progress; |
8d59ce07 | 22 | import be.nikiroo.utils.streams.MarkableFileInputStream; |
08fe2e33 NR |
23 | |
24 | /** | |
25 | * Support class for local stories encoded in textual format, with a few rules: | |
26 | * <ul> | |
27 | * <li>The title must be on the first line</li> | |
28 | * <li>The author (preceded by nothing, "by " or "©") must be on the second | |
29 | * line, possibly with the publication date in parenthesis (i.e., " | |
30 | * <tt>By Unknown (3rd October 1998)</tt>")</li> | |
31 | * <li>Chapters must be declared with "<tt>Chapter x</tt>" or " | |
32 | * <tt>Chapter x: NAME OF THE CHAPTER</tt>", where "<tt>x</tt>" is the chapter | |
33 | * number</li> | |
34 | * <li>A description of the story must be given as chapter number 0</li> | |
35 | * <li>A cover may be present, with the same filename but a PNG, JPEG or JPG | |
48f14dc9 | 36 | * extension</li> |
08fe2e33 NR |
37 | * </ul> |
38 | * | |
39 | * @author niki | |
40 | */ | |
7445f856 NR |
41 | class Text extends BasicSupport { |
42 | private File sourceFile; | |
43 | private InputStream in; | |
44 | ||
45 | protected File getSourceFile() { | |
46 | return sourceFile; | |
47 | } | |
48 | ||
49 | protected InputStream getInput() { | |
50 | if (in != null) { | |
51 | try { | |
52 | in.reset(); | |
53 | } catch (IOException e) { | |
d66deb8d | 54 | Instance.getInstance().getTraceHandler().error(new IOException("Cannot reset the Text stream", e)); |
7445f856 NR |
55 | } |
56 | ||
57 | return in; | |
58 | } | |
59 | ||
60 | return null; | |
61 | } | |
62 | ||
08fe2e33 NR |
63 | @Override |
64 | protected boolean isHtml() { | |
65 | return false; | |
66 | } | |
67 | ||
08fe2e33 | 68 | @Override |
7445f856 NR |
69 | protected Document loadDocument(URL source) throws IOException { |
70 | try { | |
71 | sourceFile = new File(source.toURI()); | |
67837328 | 72 | in = new MarkableFileInputStream(sourceFile); |
7445f856 NR |
73 | } catch (URISyntaxException e) { |
74 | throw new IOException("Cannot load the text document: " + source); | |
75 | } | |
76 | ||
77 | return null; | |
78 | } | |
79 | ||
80 | @Override | |
81 | protected MetaData getMeta() throws IOException { | |
68686a37 NR |
82 | MetaData meta = new MetaData(); |
83 | ||
7445f856 NR |
84 | meta.setTitle(getTitle()); |
85 | meta.setAuthor(getAuthor()); | |
bff19b54 | 86 | meta.setDate(bsHelper.formatDate(getDate())); |
68686a37 | 87 | meta.setTags(new ArrayList<String>()); |
7445f856 | 88 | meta.setUrl(getSourceFile().toURI().toURL().toString()); |
7445f856 | 89 | meta.setUuid(getSourceFile().toString()); |
68686a37 | 90 | meta.setLuid(""); |
7445f856 NR |
91 | meta.setLang(getLang()); // default is EN |
92 | meta.setSubject(getSourceFile().getParentFile().getName()); | |
68686a37 | 93 | meta.setImageDocument(false); |
7445f856 | 94 | meta.setCover(getCover(getSourceFile())); |
31e27ee3 | 95 | |
68686a37 | 96 | return meta; |
08fe2e33 NR |
97 | } |
98 | ||
7445f856 | 99 | private String getLang() { |
75a6a3ea | 100 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 101 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 102 | scan.useDelimiter("\\n"); |
bb7d9ea9 NR |
103 | if (scan.hasNext()) |
104 | scan.next(); // Title | |
105 | if (scan.hasNext()) | |
106 | scan.next(); // Author (Date) | |
107 | String chapter0 = ""; | |
108 | while (scan.hasNext() && chapter0.isEmpty()) { | |
08fe2e33 NR |
109 | chapter0 = scan.next(); |
110 | } | |
111 | ||
22848428 NR |
112 | String lang = detectChapter(chapter0, 0); |
113 | if (lang == null) { | |
114 | // No description?? | |
115 | lang = detectChapter(chapter0, 1); | |
116 | } | |
117 | ||
08fe2e33 | 118 | if (lang == null) { |
276f95c6 | 119 | lang = "en"; |
08fe2e33 | 120 | } else { |
276f95c6 | 121 | lang = lang.toLowerCase(); |
08fe2e33 NR |
122 | } |
123 | ||
124 | return lang; | |
125 | } | |
126 | ||
7445f856 | 127 | private String getTitle() { |
75a6a3ea | 128 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 129 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 130 | scan.useDelimiter("\\n"); |
bb7d9ea9 NR |
131 | if (scan.hasNext()) |
132 | return scan.next(); | |
133 | return ""; | |
08fe2e33 NR |
134 | } |
135 | ||
7445f856 | 136 | private String getAuthor() { |
75a6a3ea | 137 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 138 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 139 | scan.useDelimiter("\\n"); |
bb7d9ea9 NR |
140 | if (scan.hasNext()) |
141 | scan.next(); | |
142 | String authorDate = ""; | |
143 | if (scan.hasNext()) | |
144 | authorDate = scan.next(); | |
08fe2e33 NR |
145 | |
146 | String author = authorDate; | |
147 | int pos = authorDate.indexOf('('); | |
148 | if (pos >= 0) { | |
149 | author = authorDate.substring(0, pos); | |
150 | } | |
151 | ||
8d59ce07 | 152 | return bsHelper.fixAuthor(author); |
08fe2e33 NR |
153 | } |
154 | ||
7445f856 | 155 | private String getDate() { |
75a6a3ea | 156 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 157 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 158 | scan.useDelimiter("\\n"); |
bb7d9ea9 NR |
159 | if (scan.hasNext()) |
160 | scan.next(); | |
161 | String authorDate = ""; | |
162 | if (scan.hasNext()) | |
163 | authorDate = scan.next(); | |
08fe2e33 NR |
164 | |
165 | String date = ""; | |
166 | int pos = authorDate.indexOf('('); | |
167 | if (pos >= 0) { | |
168 | date = authorDate.substring(pos + 1).trim(); | |
169 | pos = date.lastIndexOf(')'); | |
170 | if (pos >= 0) { | |
171 | date = date.substring(0, pos).trim(); | |
172 | } | |
173 | } | |
174 | ||
175 | return date; | |
176 | } | |
177 | ||
178 | @Override | |
7445f856 | 179 | protected String getDesc() throws IOException { |
34f8780d | 180 | return getChapterContent(null, 0, null).trim(); |
08fe2e33 NR |
181 | } |
182 | ||
31e27ee3 | 183 | protected Image getCover(File sourceFile) { |
7445f856 | 184 | String path = sourceFile.getName(); |
08fe2e33 NR |
185 | |
186 | for (String ext : new String[] { ".txt", ".text", ".story" }) { | |
187 | if (path.endsWith(ext)) { | |
188 | path = path.substring(0, path.length() - ext.length()); | |
189 | } | |
190 | } | |
191 | ||
32097898 | 192 | Image cover = bsImages.getImage(this, sourceFile.getParentFile(), path); |
81b5e730 NR |
193 | if (cover != null) { |
194 | try { | |
d66deb8d | 195 | File tmp = Instance.getInstance().getTempFiles().createTempFile("test_cover_image"); |
81b5e730 NR |
196 | ImageUtils.getInstance().saveAsImage(cover, tmp, "png"); |
197 | tmp.delete(); | |
198 | } catch (IOException e) { | |
199 | cover = null; | |
200 | } | |
201 | } | |
202 | ||
203 | return cover; | |
08fe2e33 NR |
204 | } |
205 | ||
206 | @Override | |
7445f856 NR |
207 | protected List<Entry<String, URL>> getChapters(Progress pg) |
208 | throws IOException { | |
08fe2e33 | 209 | List<Entry<String, URL>> chaps = new ArrayList<Entry<String, URL>>(); |
75a6a3ea | 210 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 211 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 212 | scan.useDelimiter("\\n"); |
75a6a3ea | 213 | String line = "first is not empty"; |
08fe2e33 | 214 | while (scan.hasNext()) { |
75a6a3ea NR |
215 | boolean prevLineEmpty = line.trim().isEmpty(); |
216 | line = scan.next(); | |
22848428 NR |
217 | if (prevLineEmpty && detectChapter(line, chaps.size() + 1) != null) { |
218 | String chapName = Integer.toString(chaps.size() + 1); | |
219 | int pos = line.indexOf(':'); | |
220 | if (pos >= 0 && pos + 1 < line.length()) { | |
221 | chapName = line.substring(pos + 1).trim(); | |
222 | } | |
08fe2e33 | 223 | |
7445f856 NR |
224 | chaps.add(new AbstractMap.SimpleEntry<String, URL>(// |
225 | chapName, // | |
226 | getSourceFile().toURI().toURL())); | |
08fe2e33 | 227 | } |
08fe2e33 | 228 | } |
75a6a3ea | 229 | |
08fe2e33 NR |
230 | return chaps; |
231 | } | |
232 | ||
233 | @Override | |
7445f856 NR |
234 | protected String getChapterContent(URL source, int number, Progress pg) |
235 | throws IOException { | |
08fe2e33 | 236 | StringBuilder builder = new StringBuilder(); |
75a6a3ea | 237 | @SuppressWarnings("resource") // cannot close, or we loose getInput()! |
7445f856 | 238 | Scanner scan = new Scanner(getInput(), "UTF-8"); |
08fe2e33 | 239 | scan.useDelimiter("\\n"); |
a72efc14 NR |
240 | if (scan.hasNext()) |
241 | scan.next(); // title | |
242 | if (scan.hasNext()) | |
243 | scan.next(); // author | |
244 | if (scan.hasNext()) | |
245 | scan.next(); // date or empty | |
75a6a3ea NR |
246 | Boolean inChap = null; |
247 | String line = ""; | |
08fe2e33 | 248 | while (scan.hasNext()) { |
75a6a3ea NR |
249 | if (number == 0 && !line.trim().isEmpty()) { |
250 | // We found pre-chapter content, we are checking for | |
251 | // Chapter 0 (fake chapter) --> keep the content | |
252 | if (inChap == null) | |
253 | inChap = true; | |
254 | } | |
255 | line = scan.next(); | |
256 | if ((inChap == null || !inChap) && detectChapter(line, number) != null) { | |
68686a37 | 257 | inChap = true; |
32097898 | 258 | } else if (detectChapter(line, number + 1) != null) { |
68686a37 | 259 | break; |
75a6a3ea | 260 | } else if (inChap != null && inChap) { |
68686a37 NR |
261 | builder.append(line); |
262 | builder.append("\n"); | |
08fe2e33 | 263 | } |
08fe2e33 NR |
264 | } |
265 | ||
266 | return builder.toString(); | |
267 | } | |
268 | ||
7445f856 NR |
269 | @Override |
270 | protected void close() { | |
271 | InputStream in = getInput(); | |
272 | if (in != null) { | |
273 | try { | |
274 | in.close(); | |
275 | } catch (IOException e) { | |
d66deb8d NR |
276 | Instance.getInstance().getTraceHandler() |
277 | .error(new IOException("Cannot close the text source file input", e)); | |
7445f856 NR |
278 | } |
279 | } | |
280 | ||
281 | super.close(); | |
282 | } | |
283 | ||
08fe2e33 NR |
284 | @Override |
285 | protected boolean supports(URL url) { | |
86d49dbc NR |
286 | return supports(url, false); |
287 | } | |
288 | ||
289 | /** | |
290 | * Check if we supports this {@link URL}, that is, if the info file can be | |
291 | * found OR not found. | |
3ddb5591 NR |
292 | * <p> |
293 | * It must also be a file, not another kind of URL. | |
86d49dbc NR |
294 | * |
295 | * @param url | |
296 | * the {@link URL} to check | |
297 | * @param info | |
298 | * TRUE to require the info file, FALSE to forbid the info file | |
299 | * | |
300 | * @return TRUE if it is supported | |
301 | */ | |
302 | protected boolean supports(URL url, boolean info) { | |
3ddb5591 NR |
303 | if (!"file".equals(url.getProtocol())) { |
304 | return false; | |
305 | } | |
08fe2e33 | 306 | |
3ddb5591 NR |
307 | boolean infoPresent = false; |
308 | File file; | |
309 | try { | |
310 | file = new File(url.toURI()); | |
311 | file = assureNoTxt(file); | |
312 | file = new File(file.getPath() + ".info"); | |
313 | } catch (URISyntaxException e) { | |
d66deb8d | 314 | Instance.getInstance().getTraceHandler().error(e); |
3ddb5591 | 315 | file = null; |
08fe2e33 NR |
316 | } |
317 | ||
3ddb5591 NR |
318 | infoPresent = (file != null && file.exists()); |
319 | ||
86d49dbc NR |
320 | return infoPresent == info; |
321 | } | |
322 | ||
323 | /** | |
31e27ee3 | 324 | * Remove the ".txt" (or ".text") extension if it is present. |
86d49dbc NR |
325 | * |
326 | * @param file | |
327 | * the file to process | |
328 | * | |
329 | * @return the same file or a copy of it without the ".txt" extension if it | |
330 | * was present | |
331 | */ | |
332 | protected File assureNoTxt(File file) { | |
31e27ee3 NR |
333 | for (String ext : new String[] { ".txt", ".text" }) { |
334 | if (file.getName().endsWith(ext)) { | |
335 | file = new File(file.getPath().substring(0, | |
336 | file.getPath().length() - ext.length())); | |
337 | } | |
86d49dbc NR |
338 | } |
339 | ||
340 | return file; | |
08fe2e33 NR |
341 | } |
342 | ||
08fe2e33 NR |
343 | /** |
344 | * Check if the given line looks like the given starting chapter in a | |
345 | * supported language, and return the language if it does (or NULL if not). | |
346 | * | |
347 | * @param line | |
348 | * the line to check | |
32097898 NR |
349 | * @param number |
350 | * the specific chapter number to check for | |
08fe2e33 NR |
351 | * |
352 | * @return the language or NULL | |
353 | */ | |
7445f856 | 354 | static private String detectChapter(String line, int number) { |
08fe2e33 | 355 | line = line.toUpperCase(); |
d66deb8d NR |
356 | for (String lang : Instance.getInstance().getConfig().getList(Config.CONF_CHAPTER)) { |
357 | String chapter = Instance.getInstance().getConfig().getStringX(Config.CONF_CHAPTER, lang); | |
08fe2e33 NR |
358 | if (chapter != null && !chapter.isEmpty()) { |
359 | chapter = chapter.toUpperCase() + " "; | |
360 | if (line.startsWith(chapter)) { | |
22848428 NR |
361 | // We want "[CHAPTER] [number]: [name]", with ": [name]" |
362 | // optional | |
363 | String test = line.substring(chapter.length()).trim(); | |
32097898 NR |
364 | |
365 | String possibleNum = test.trim(); | |
366 | if (possibleNum.indexOf(':') > 0) { | |
367 | possibleNum = possibleNum.substring(0, | |
368 | possibleNum.indexOf(':')).trim(); | |
369 | } | |
370 | ||
22848428 NR |
371 | if (test.startsWith(Integer.toString(number))) { |
372 | test = test | |
373 | .substring(Integer.toString(number).length()) | |
374 | .trim(); | |
375 | if (test.isEmpty() || test.startsWith(":")) { | |
376 | return lang; | |
08fe2e33 | 377 | } |
08fe2e33 NR |
378 | } |
379 | } | |
380 | } | |
381 | } | |
382 | ||
383 | return null; | |
384 | } | |
385 | } |