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