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