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