update from master
[fanfix.git] / supported / Text.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix.supported;
2
3import java.io.File;
4import java.io.IOException;
5import java.io.InputStream;
6import java.net.URISyntaxException;
7import java.net.URL;
7445f856 8import java.util.AbstractMap;
08fe2e33
NR
9import java.util.ArrayList;
10import java.util.List;
11import java.util.Map.Entry;
12import java.util.Scanner;
13
7445f856
NR
14import org.jsoup.nodes.Document;
15
08fe2e33
NR
16import be.nikiroo.fanfix.Instance;
17import be.nikiroo.fanfix.bundles.Config;
68686a37 18import be.nikiroo.fanfix.data.MetaData;
16a81ef7 19import be.nikiroo.utils.Image;
81b5e730 20import be.nikiroo.utils.ImageUtils;
ed08c171 21import be.nikiroo.utils.Progress;
8d59ce07 22import 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
41class 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}