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