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