Fix epub compatibility + cover image ext
[fanfix.git] / src / be / nikiroo / fanfix / supported / BasicSupport.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix.supported;
2
68e370a4 3import java.io.BufferedReader;
08fe2e33
NR
4import java.io.ByteArrayInputStream;
5import java.io.File;
6import java.io.IOException;
7import java.io.InputStream;
68e370a4 8import java.io.InputStreamReader;
08fe2e33
NR
9import java.net.MalformedURLException;
10import java.net.URL;
08fe2e33 11import java.util.ArrayList;
793f1071 12import java.util.Date;
08fe2e33
NR
13import java.util.HashMap;
14import java.util.List;
15import java.util.Map;
16import java.util.Map.Entry;
17import java.util.Scanner;
18
19import be.nikiroo.fanfix.Instance;
20import be.nikiroo.fanfix.bundles.Config;
21import be.nikiroo.fanfix.bundles.StringId;
22import be.nikiroo.fanfix.data.Chapter;
23import be.nikiroo.fanfix.data.MetaData;
24import be.nikiroo.fanfix.data.Paragraph;
08fe2e33 25import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
9252c65e 26import be.nikiroo.fanfix.data.Story;
16a81ef7 27import be.nikiroo.utils.Image;
3b2b638f 28import be.nikiroo.utils.Progress;
08fe2e33
NR
29import be.nikiroo.utils.StringUtils;
30
31/**
32 * This class is the base class used by the other support classes. It can be
33 * used outside of this package, and have static method that you can use to get
34 * access to the correct support class.
35 * <p>
36 * It will be used with 'resources' (usually web pages or files).
37 *
38 * @author niki
39 */
40public abstract class BasicSupport {
41 /**
42 * The supported input types for which we can get a {@link BasicSupport}
43 * object.
44 *
45 * @author niki
46 */
47 public enum SupportType {
48 /** EPUB files created with this program */
49 EPUB,
50 /** Pure text file with some rules */
51 TEXT,
52 /** TEXT but with associated .info file */
53 INFO_TEXT,
54 /** My Little Pony fanfictions */
55 FIMFICTION,
56 /** Fanfictions from a lot of different universes */
57 FANFICTION,
58 /** Website with lots of Mangas */
59 MANGAFOX,
60 /** Furry website with comics support */
61 E621,
a4143cd7
NR
62 /** Furry website with stories */
63 YIFFSTAR,
f0608ab1
NR
64 /** Comics and images groups, mostly but not only NSFW */
65 E_HENTAI,
08fe2e33 66 /** CBZ files */
373da363
NR
67 CBZ,
68 /** HTML files */
69 HTML;
08fe2e33
NR
70
71 /**
72 * A description of this support type (more information than the
73 * {@link BasicSupport#getSourceName()}).
74 *
75 * @return the description
76 */
77 public String getDesc() {
78 String desc = Instance.getTrans().getStringX(StringId.INPUT_DESC,
79 this.name());
80
81 if (desc == null) {
82 desc = Instance.getTrans().getString(StringId.INPUT_DESC, this);
83 }
84
85 return desc;
86 }
87
88 /**
89 * The name of this support type (a short version).
90 *
91 * @return the name
92 */
93 public String getSourceName() {
94 BasicSupport support = BasicSupport.getSupport(this);
95 if (support != null) {
96 return support.getSourceName();
97 }
98
99 return null;
100 }
101
102 @Override
103 public String toString() {
104 return super.toString().toLowerCase();
105 }
106
107 /**
0efd25e3
NR
108 * Call {@link SupportType#valueOf(String)} after conversion to upper
109 * case.
08fe2e33
NR
110 *
111 * @param typeName
112 * the possible type name
113 *
114 * @return NULL or the type
115 */
116 public static SupportType valueOfUC(String typeName) {
117 return SupportType.valueOf(typeName == null ? null : typeName
118 .toUpperCase());
119 }
120
121 /**
0efd25e3
NR
122 * Call {@link SupportType#valueOf(String)} after conversion to upper
123 * case but return NULL for NULL instead of raising exception.
08fe2e33
NR
124 *
125 * @param typeName
126 * the possible type name
127 *
128 * @return NULL or the type
129 */
130 public static SupportType valueOfNullOkUC(String typeName) {
131 if (typeName == null) {
132 return null;
133 }
134
135 return SupportType.valueOfUC(typeName);
136 }
137
138 /**
0efd25e3
NR
139 * Call {@link SupportType#valueOf(String)} after conversion to upper
140 * case but return NULL in case of error instead of raising an
141 * exception.
08fe2e33
NR
142 *
143 * @param typeName
144 * the possible type name
145 *
146 * @return NULL or the type
147 */
148 public static SupportType valueOfAllOkUC(String typeName) {
149 try {
150 return SupportType.valueOfUC(typeName);
151 } catch (Exception e) {
152 return null;
153 }
154 }
155 }
156
08fe2e33
NR
157 private InputStream in;
158 private SupportType type;
22848428 159 private URL currentReferer; // with only one 'r', as in 'HTTP'...
08fe2e33
NR
160
161 // quote chars
e8eeea0a 162 private char openQuote = Instance.getTrans().getCharacter(
08fe2e33 163 StringId.OPEN_SINGLE_QUOTE);
e8eeea0a 164 private char closeQuote = Instance.getTrans().getCharacter(
08fe2e33 165 StringId.CLOSE_SINGLE_QUOTE);
e8eeea0a 166 private char openDoubleQuote = Instance.getTrans().getCharacter(
08fe2e33 167 StringId.OPEN_DOUBLE_QUOTE);
e8eeea0a 168 private char closeDoubleQuote = Instance.getTrans().getCharacter(
08fe2e33
NR
169 StringId.CLOSE_DOUBLE_QUOTE);
170
171 /**
172 * The name of this support class.
173 *
174 * @return the name
175 */
176 protected abstract String getSourceName();
177
178 /**
179 * Check if the given resource is supported by this {@link BasicSupport}.
180 *
181 * @param url
182 * the resource to check for
183 *
184 * @return TRUE if it is
185 */
186 protected abstract boolean supports(URL url);
187
188 /**
189 * Return TRUE if the support will return HTML encoded content values for
190 * the chapters content.
191 *
192 * @return TRUE for HTML
193 */
194 protected abstract boolean isHtml();
195
0efd25e3
NR
196 /**
197 * Return the {@link MetaData} of this story.
198 *
199 * @param source
200 * the source of the story
201 * @param in
202 * the input (the main resource)
203 *
776ad3c6 204 * @return the associated {@link MetaData}, never NULL
0efd25e3
NR
205 *
206 * @throws IOException
207 * in case of I/O error
208 */
68686a37 209 protected abstract MetaData getMeta(URL source, InputStream in)
08fe2e33
NR
210 throws IOException;
211
212 /**
213 * Return the story description.
214 *
215 * @param source
216 * the source of the story
217 * @param in
218 * the input (the main resource)
219 *
220 * @return the description
221 *
222 * @throws IOException
223 * in case of I/O error
224 */
225 protected abstract String getDesc(URL source, InputStream in)
226 throws IOException;
227
08fe2e33
NR
228 /**
229 * Return the list of chapters (name and resource).
230 *
231 * @param source
232 * the source of the story
233 * @param in
234 * the input (the main resource)
ed08c171
NR
235 * @param pg
236 * the optional progress reporter
08fe2e33
NR
237 *
238 * @return the chapters
239 *
240 * @throws IOException
241 * in case of I/O error
242 */
243 protected abstract List<Entry<String, URL>> getChapters(URL source,
ed08c171 244 InputStream in, Progress pg) throws IOException;
08fe2e33
NR
245
246 /**
247 * Return the content of the chapter (possibly HTML encoded, if
248 * {@link BasicSupport#isHtml()} is TRUE).
249 *
250 * @param source
251 * the source of the story
252 * @param in
253 * the input (the main resource)
254 * @param number
255 * the chapter number
ed08c171
NR
256 * @param pg
257 * the optional progress reporter
08fe2e33
NR
258 *
259 * @return the content
260 *
261 * @throws IOException
262 * in case of I/O error
263 */
264 protected abstract String getChapterContent(URL source, InputStream in,
ed08c171 265 int number, Progress pg) throws IOException;
08fe2e33 266
6e06d2cc
NR
267 /**
268 * Log into the support (can be a no-op depending upon the support).
269 *
270 * @throws IOException
271 * in case of I/O error
272 */
315f14ae 273 @SuppressWarnings("unused")
6e06d2cc 274 public void login() throws IOException {
6e06d2cc
NR
275 }
276
08fe2e33
NR
277 /**
278 * Return the list of cookies (values included) that must be used to
279 * correctly fetch the resources.
280 * <p>
281 * You are expected to call the super method implementation if you override
282 * it.
283 *
284 * @return the cookies
285 */
315f14ae 286 public Map<String, String> getCookies() {
08fe2e33
NR
287 return new HashMap<String, String>();
288 }
289
315f14ae
NR
290 /**
291 * OAuth authorisation (aka, "bearer XXXXXXX").
292 *
293 * @return the OAuth string
294 */
295 public String getOAuth() {
296 return null;
297 }
298
a4143cd7
NR
299 /**
300 * Return the canonical form of the main {@link URL}.
301 *
302 * @param source
303 * the source {@link URL}
304 *
305 * @return the canonical form of this {@link URL}
306 *
307 * @throws IOException
308 * in case of I/O error
309 */
315f14ae 310 @SuppressWarnings("unused")
a4143cd7
NR
311 public URL getCanonicalUrl(URL source) throws IOException {
312 return source;
313 }
314
08fe2e33
NR
315 /**
316 * Process the given story resource into a partially filled {@link Story}
317 * object containing the name and metadata, except for the description.
318 *
319 * @param url
320 * the story resource
321 *
322 * @return the {@link Story}
323 *
324 * @throws IOException
325 * in case of I/O error
326 */
327 public Story processMeta(URL url) throws IOException {
ed08c171 328 return processMeta(url, true, false, null);
08fe2e33
NR
329 }
330
331 /**
332 * Process the given story resource into a partially filled {@link Story}
333 * object containing the name and metadata.
334 *
335 * @param url
336 * the story resource
08fe2e33
NR
337 * @param close
338 * close "this" and "in" when done
0efd25e3
NR
339 * @param getDesc
340 * retrieve the description of the story, or not
ed08c171
NR
341 * @param pg
342 * the optional progress reporter
08fe2e33 343 *
776ad3c6 344 * @return the {@link Story}, never NULL
08fe2e33
NR
345 *
346 * @throws IOException
347 * in case of I/O error
348 */
ed08c171
NR
349 protected Story processMeta(URL url, boolean close, boolean getDesc,
350 Progress pg) throws IOException {
351 if (pg == null) {
352 pg = new Progress();
353 } else {
354 pg.setMinMax(0, 100);
355 }
356
6e06d2cc 357 login();
ed08c171 358 pg.setProgress(10);
6e06d2cc 359
a4143cd7
NR
360 url = getCanonicalUrl(url);
361
362 setCurrentReferer(url);
363
315f14ae 364 in = openInput(url); // NULL allowed here
08fe2e33 365 try {
68686a37 366 preprocess(url, getInput());
ed08c171 367 pg.setProgress(30);
08fe2e33
NR
368
369 Story story = new Story();
68686a37 370 MetaData meta = getMeta(url, getInput());
793f1071
NR
371 if (meta.getCreationDate() == null
372 || meta.getCreationDate().isEmpty()) {
373 meta.setCreationDate(StringUtils.fromTime(new Date().getTime()));
374 }
68686a37
NR
375 story.setMeta(meta);
376
ed08c171
NR
377 pg.setProgress(50);
378
211f7ddb 379 if (meta.getCover() == null) {
68686a37
NR
380 meta.setCover(getDefaultCover(meta.getSubject()));
381 }
08fe2e33 382
ed08c171
NR
383 pg.setProgress(60);
384
08fe2e33
NR
385 if (getDesc) {
386 String descChapterName = Instance.getTrans().getString(
387 StringId.DESCRIPTION);
388 story.getMeta().setResume(
389 makeChapter(url, 0, descChapterName,
ed08c171 390 getDesc(url, getInput()), null));
08fe2e33
NR
391 }
392
ed08c171 393 pg.setProgress(100);
08fe2e33
NR
394 return story;
395 } finally {
396 if (close) {
397 try {
398 close();
399 } catch (IOException e) {
62c63b07 400 Instance.getTraceHandler().error(e);
08fe2e33
NR
401 }
402
403 if (in != null) {
404 in.close();
405 }
406 }
a4143cd7
NR
407
408 setCurrentReferer(null);
08fe2e33
NR
409 }
410 }
411
412 /**
413 * Process the given story resource into a fully filled {@link Story}
414 * object.
415 *
416 * @param url
417 * the story resource
92fb0719
NR
418 * @param pg
419 * the optional progress reporter
08fe2e33 420 *
776ad3c6 421 * @return the {@link Story}, never NULL
08fe2e33
NR
422 *
423 * @throws IOException
424 * in case of I/O error
425 */
92fb0719
NR
426 public Story process(URL url, Progress pg) throws IOException {
427 if (pg == null) {
428 pg = new Progress();
429 } else {
430 pg.setMinMax(0, 100);
431 }
432
a4143cd7 433 url = getCanonicalUrl(url);
92fb0719 434 pg.setProgress(1);
08fe2e33 435 try {
ed08c171
NR
436 Progress pgMeta = new Progress();
437 pg.addProgress(pgMeta, 10);
438 Story story = processMeta(url, false, true, pgMeta);
439 if (!pgMeta.isDone()) {
440 pgMeta.setProgress(pgMeta.getMax()); // 10%
441 }
442
754a5bc2
NR
443 pg.setName("Retrieving " + story.getMeta().getTitle());
444
a4143cd7
NR
445 setCurrentReferer(url);
446
ed08c171
NR
447 Progress pgGetChapters = new Progress();
448 pg.addProgress(pgGetChapters, 10);
08fe2e33 449 story.setChapters(new ArrayList<Chapter>());
ed08c171
NR
450 List<Entry<String, URL>> chapters = getChapters(url, getInput(),
451 pgGetChapters);
452 if (!pgGetChapters.isDone()) {
453 pgGetChapters.setProgress(pgGetChapters.getMax()); // 20%
454 }
08fe2e33 455
08fe2e33 456 if (chapters != null) {
ed08c171
NR
457 Progress pgChaps = new Progress("Extracting chapters", 0,
458 chapters.size() * 300);
92fb0719
NR
459 pg.addProgress(pgChaps, 80);
460
793f1071 461 long words = 0;
ed08c171 462 int i = 1;
08fe2e33 463 for (Entry<String, URL> chap : chapters) {
ed08c171 464 pgChaps.setName("Extracting chapter " + i);
315f14ae
NR
465 InputStream chapIn = null;
466 if (chap.getValue() != null) {
467 setCurrentReferer(chap.getValue());
468 chapIn = Instance.getCache().open(chap.getValue(),
cbd62024 469 this, false);
315f14ae 470 }
ed08c171 471 pgChaps.setProgress(i * 100);
08fe2e33 472 try {
ed08c171
NR
473 Progress pgGetChapterContent = new Progress();
474 Progress pgMakeChapter = new Progress();
475 pgChaps.addProgress(pgGetChapterContent, 100);
476 pgChaps.addProgress(pgMakeChapter, 100);
477
478 String content = getChapterContent(url, chapIn, i,
479 pgGetChapterContent);
480 if (!pgGetChapterContent.isDone()) {
481 pgGetChapterContent.setProgress(pgGetChapterContent
482 .getMax());
483 }
484
793f1071 485 Chapter cc = makeChapter(url, i, chap.getKey(),
ed08c171
NR
486 content, pgMakeChapter);
487 if (!pgMakeChapter.isDone()) {
488 pgMakeChapter.setProgress(pgMakeChapter.getMax());
489 }
490
793f1071
NR
491 words += cc.getWords();
492 story.getChapters().add(cc);
776ad3c6 493 story.getMeta().setWords(words);
08fe2e33 494 } finally {
315f14ae
NR
495 if (chapIn != null) {
496 chapIn.close();
497 }
08fe2e33 498 }
a6395bef 499
ed08c171 500 i++;
08fe2e33 501 }
ed08c171
NR
502
503 pgChaps.setName("Extracting chapters");
92fb0719 504 } else {
ed08c171 505 pg.setProgress(80);
08fe2e33
NR
506 }
507
508 return story;
509
510 } finally {
511 try {
512 close();
513 } catch (IOException e) {
62c63b07 514 Instance.getTraceHandler().error(e);
08fe2e33
NR
515 }
516
517 if (in != null) {
518 in.close();
519 }
520
a4143cd7 521 setCurrentReferer(null);
08fe2e33
NR
522 }
523 }
524
525 /**
a4143cd7 526 * The support type.
08fe2e33
NR
527 *
528 * @return the type
529 */
530 public SupportType getType() {
531 return type;
532 }
533
534 /**
535 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
536 * the current {@link URL} we work on.
537 *
538 * @return the referer
539 */
540 public URL getCurrentReferer() {
541 return currentReferer;
542 }
543
544 /**
545 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
546 * the current {@link URL} we work on.
547 *
548 * @param currentReferer
549 * the new referer
550 */
551 protected void setCurrentReferer(URL currentReferer) {
552 this.currentReferer = currentReferer;
553 }
554
555 /**
556 * The support type.
557 *
558 * @param type
559 * the new type
560 *
561 * @return this
562 */
563 protected BasicSupport setType(SupportType type) {
564 this.type = type;
565 return this;
566 }
567
568 /**
68686a37 569 * Prepare the support if needed before processing.
08fe2e33
NR
570 *
571 * @param source
572 * the source of the story
573 * @param in
574 * the input (the main resource)
575 *
08fe2e33
NR
576 * @throws IOException
577 * on I/O error
578 */
211f7ddb 579 @SuppressWarnings("unused")
68686a37 580 protected void preprocess(URL source, InputStream in) throws IOException {
08fe2e33
NR
581 }
582
583 /**
584 * Now that we have processed the {@link Story}, close the resources if any.
585 *
586 * @throws IOException
587 * on I/O error
588 */
315f14ae 589 @SuppressWarnings("unused")
08fe2e33
NR
590 protected void close() throws IOException {
591 }
592
593 /**
594 * Create a {@link Chapter} object from the given information, formatting
595 * the content as it should be.
596 *
0efd25e3
NR
597 * @param source
598 * the source of the story
08fe2e33
NR
599 * @param number
600 * the chapter number
601 * @param name
602 * the chapter name
603 * @param content
604 * the chapter content
ed08c171
NR
605 * @param pg
606 * the optional progress reporter
08fe2e33
NR
607 *
608 * @return the {@link Chapter}
609 *
610 * @throws IOException
611 * in case of I/O error
612 */
613 protected Chapter makeChapter(URL source, int number, String name,
ed08c171 614 String content, Progress pg) throws IOException {
08fe2e33 615 // Chapter name: process it correctly, then remove the possible
f60df2f1
NR
616 // redundant "Chapter x: " in front of it, or "-" (as in
617 // "Chapter 5: - Fun!" after the ": " was automatically added)
08fe2e33
NR
618 String chapterName = processPara(name).getContent().trim();
619 for (String lang : Instance.getConfig().getString(Config.CHAPTER)
620 .split(",")) {
621 String chapterWord = Instance.getConfig().getStringX(
622 Config.CHAPTER, lang);
623 if (chapterName.startsWith(chapterWord)) {
624 chapterName = chapterName.substring(chapterWord.length())
625 .trim();
626 break;
627 }
628 }
629
630 if (chapterName.startsWith(Integer.toString(number))) {
631 chapterName = chapterName.substring(
632 Integer.toString(number).length()).trim();
633 }
634
f60df2f1 635 while (chapterName.startsWith(":") || chapterName.startsWith("-")) {
08fe2e33
NR
636 chapterName = chapterName.substring(1).trim();
637 }
638 //
639
640 Chapter chap = new Chapter(number, chapterName);
641
68e370a4 642 if (content != null) {
ed08c171 643 List<Paragraph> paras = makeParagraphs(source, content, pg);
793f1071
NR
644 long words = 0;
645 for (Paragraph para : paras) {
646 words += para.getWords();
647 }
648 chap.setParagraphs(paras);
649 chap.setWords(words);
08fe2e33
NR
650 }
651
68e370a4
NR
652 return chap;
653
654 }
655
656 /**
657 * Convert the given content into {@link Paragraph}s.
658 *
659 * @param source
660 * the source URL of the story
661 * @param content
662 * the textual content
ed08c171
NR
663 * @param pg
664 * the optional progress reporter
68e370a4
NR
665 *
666 * @return the {@link Paragraph}s
667 *
668 * @throws IOException
669 * in case of I/O error
670 */
ed08c171
NR
671 protected List<Paragraph> makeParagraphs(URL source, String content,
672 Progress pg) throws IOException {
673 if (pg == null) {
674 pg = new Progress();
675 }
676
08fe2e33
NR
677 if (isHtml()) {
678 // Special <HR> processing:
679 content = content.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
d5a7153c 680 "<br/>* * *<br/>");
08fe2e33
NR
681 }
682
68e370a4 683 List<Paragraph> paras = new ArrayList<Paragraph>();
08fe2e33 684
d5a7153c
NR
685 if (content != null && !content.trim().isEmpty()) {
686 if (isHtml()) {
ed08c171
NR
687 String[] tab = content.split("(<p>|</p>|<br>|<br/>)");
688 pg.setMinMax(0, tab.length);
689 int i = 1;
690 for (String line : tab) {
691 if (line.startsWith("[") && line.endsWith("]")) {
692 pg.setName("Extracting image " + i);
693 }
d5a7153c 694 paras.add(makeParagraph(source, line.trim()));
ed08c171 695 pg.setProgress(i++);
d5a7153c 696 }
ed08c171 697 pg.setName(null);
d5a7153c 698 } else {
ed08c171 699 List<String> lines = new ArrayList<String>();
d5a7153c
NR
700 BufferedReader buff = null;
701 try {
702 buff = new BufferedReader(
703 new InputStreamReader(new ByteArrayInputStream(
704 content.getBytes("UTF-8")), "UTF-8"));
705 for (String line = buff.readLine(); line != null; line = buff
706 .readLine()) {
ed08c171 707 lines.add(line.trim());
68e370a4 708 }
d5a7153c
NR
709 } finally {
710 if (buff != null) {
711 buff.close();
68e370a4 712 }
08fe2e33 713 }
ed08c171
NR
714
715 pg.setMinMax(0, lines.size());
716 int i = 0;
717 for (String line : lines) {
718 if (line.startsWith("[") && line.endsWith("]")) {
719 pg.setName("Extracting image " + i);
720 }
721 paras.add(makeParagraph(source, line));
722 pg.setProgress(i++);
723 }
724 pg.setName(null);
08fe2e33
NR
725 }
726
d5a7153c
NR
727 // Check quotes for "bad" format
728 List<Paragraph> newParas = new ArrayList<Paragraph>();
729 for (Paragraph para : paras) {
730 newParas.addAll(requotify(para));
731 }
732 paras = newParas;
08fe2e33 733
d5a7153c
NR
734 // Remove double blanks/brks
735 fixBlanksBreaks(paras);
736 }
08fe2e33 737
68e370a4
NR
738 return paras;
739 }
08fe2e33 740
d5a7153c
NR
741 /**
742 * Convert the given line into a single {@link Paragraph}.
743 *
744 * @param source
745 * the source URL of the story
746 * @param line
747 * the textual content of the paragraph
748 *
749 * @return the {@link Paragraph}
750 */
751 private Paragraph makeParagraph(URL source, String line) {
16a81ef7 752 Image image = null;
d5a7153c 753 if (line.startsWith("[") && line.endsWith("]")) {
2a25f781
NR
754 image = getImage(this, source, line.substring(1, line.length() - 1)
755 .trim());
d5a7153c
NR
756 }
757
758 if (image != null) {
759 return new Paragraph(image);
d5a7153c 760 }
211f7ddb
NR
761
762 return processPara(line);
d5a7153c
NR
763 }
764
68e370a4
NR
765 /**
766 * Fix the {@link ParagraphType#BLANK}s and {@link ParagraphType#BREAK}s of
767 * those {@link Paragraph}s.
768 * <p>
769 * The resulting list will not contain a starting or trailing blank/break
770 * nor 2 blanks or breaks following each other.
771 *
772 * @param paras
773 * the list of {@link Paragraph}s to fix
774 */
775 protected void fixBlanksBreaks(List<Paragraph> paras) {
776 boolean space = false;
777 boolean brk = true;
778 for (int i = 0; i < paras.size(); i++) {
779 Paragraph para = paras.get(i);
780 boolean thisSpace = para.getType() == ParagraphType.BLANK;
781 boolean thisBrk = para.getType() == ParagraphType.BREAK;
782
783 if (i > 0 && space && thisBrk) {
784 paras.remove(i - 1);
785 i--;
786 } else if ((space || brk) && (thisSpace || thisBrk)) {
787 paras.remove(i);
788 i--;
08fe2e33
NR
789 }
790
68e370a4
NR
791 space = thisSpace;
792 brk = thisBrk;
793 }
08fe2e33 794
68e370a4
NR
795 // Remove blank/brk at start
796 if (paras.size() > 0
797 && (paras.get(0).getType() == ParagraphType.BLANK || paras.get(
798 0).getType() == ParagraphType.BREAK)) {
799 paras.remove(0);
800 }
801
802 // Remove blank/brk at end
803 int last = paras.size() - 1;
804 if (paras.size() > 0
805 && (paras.get(last).getType() == ParagraphType.BLANK || paras
806 .get(last).getType() == ParagraphType.BREAK)) {
807 paras.remove(last);
08fe2e33
NR
808 }
809 }
810
68e370a4
NR
811 /**
812 * Get the default cover related to this subject (see <tt>.info</tt> files).
813 *
814 * @param subject
815 * the subject
816 *
817 * @return the cover if any, or NULL
818 */
16a81ef7 819 static Image getDefaultCover(String subject) {
68686a37
NR
820 if (subject != null && !subject.isEmpty()
821 && Instance.getCoverDir() != null) {
822 try {
823 File fileCover = new File(Instance.getCoverDir(), subject);
333f0e7b 824 return getImage(null, fileCover.toURI().toURL(), subject);
68686a37
NR
825 } catch (MalformedURLException e) {
826 }
827 }
828
829 return null;
830 }
831
08fe2e33
NR
832 /**
833 * Return the list of supported image extensions.
834 *
a4143cd7
NR
835 * @param emptyAllowed
836 * TRUE to allow an empty extension on first place, which can be
837 * used when you may already have an extension in your input but
838 * are not sure about it
839 *
08fe2e33
NR
840 * @return the extensions
841 */
68686a37 842 static String[] getImageExt(boolean emptyAllowed) {
08fe2e33
NR
843 if (emptyAllowed) {
844 return new String[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
08fe2e33 845 }
211f7ddb
NR
846
847 return new String[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
08fe2e33
NR
848 }
849
a4143cd7
NR
850 /**
851 * Check if the given resource can be a local image or a remote image, then
852 * refresh the cache with it if it is.
853 *
854 * @param source
855 * the story source
856 * @param line
857 * the resource to check
858 *
859 * @return the image if found, or NULL
860 *
861 */
16a81ef7 862 static Image getImage(BasicSupport support, URL source, String line) {
333f0e7b 863 URL url = getImageUrl(support, source, line);
68686a37 864 if (url != null) {
16a81ef7
NR
865 if ("file".equals(url.getProtocol())) {
866 if (new File(url.getPath()).isDirectory()) {
867 return null;
868 }
869 }
68686a37
NR
870 InputStream in = null;
871 try {
872 in = Instance.getCache().open(url, getSupport(url), true);
16a81ef7 873 return new Image(in);
68686a37
NR
874 } catch (IOException e) {
875 } finally {
876 if (in != null) {
877 try {
878 in.close();
879 } catch (IOException e) {
880 }
881 }
882 }
883 }
884
885 return null;
886 }
887
08fe2e33
NR
888 /**
889 * Check if the given resource can be a local image or a remote image, then
890 * refresh the cache with it if it is.
891 *
892 * @param source
893 * the story source
894 * @param line
895 * the resource to check
896 *
897 * @return the image URL if found, or NULL
898 *
899 */
333f0e7b 900 static URL getImageUrl(BasicSupport support, URL source, String line) {
08fe2e33
NR
901 URL url = null;
902
68686a37
NR
903 if (line != null) {
904 // try for files
68686a37 905 if (source != null) {
68686a37 906 try {
2ab2e40a
NR
907
908 String relPath = null;
909 String absPath = null;
910 try {
911 String path = new File(source.getFile()).getParent();
912 relPath = new File(new File(path), line.trim())
913 .getAbsolutePath();
914 } catch (Exception e) {
915 // Cannot be converted to path (one possibility to take
916 // into account: absolute path on Windows)
917 }
918 try {
919 absPath = new File(line.trim()).getAbsolutePath();
920 } catch (Exception e) {
921 // Cannot be converted to path (at all)
922 }
923
68686a37 924 for (String ext : getImageExt(true)) {
16a81ef7
NR
925 File absFile = new File(absPath + ext);
926 File relFile = new File(relPath + ext);
927 if (absPath != null && absFile.exists()
928 && absFile.isFile()) {
929 url = absFile.toURI().toURL();
930 } else if (relPath != null && relFile.exists()
931 && relFile.isFile()) {
932 url = relFile.toURI().toURL();
68686a37 933 }
08fe2e33 934 }
68686a37 935 } catch (Exception e) {
2ab2e40a 936 // Should not happen since we control the correct arguments
08fe2e33 937 }
68686a37 938 }
08fe2e33 939
68686a37
NR
940 if (url == null) {
941 // try for URLs
942 try {
08fe2e33 943 for (String ext : getImageExt(true)) {
f1fb834c
NR
944 if (Instance.getCache()
945 .check(new URL(line + ext), true)) {
08fe2e33 946 url = new URL(line + ext);
333f0e7b 947 break;
08fe2e33
NR
948 }
949 }
68686a37
NR
950
951 // try out of cache
952 if (url == null) {
953 for (String ext : getImageExt(true)) {
954 try {
955 url = new URL(line + ext);
333f0e7b 956 Instance.getCache().refresh(url, support, true);
68686a37
NR
957 break;
958 } catch (IOException e) {
959 // no image with this ext
960 url = null;
961 }
962 }
963 }
964 } catch (MalformedURLException e) {
965 // Not an url
08fe2e33 966 }
08fe2e33 967 }
08fe2e33 968
68686a37
NR
969 // refresh the cached file
970 if (url != null) {
971 try {
333f0e7b 972 Instance.getCache().refresh(url, support, true);
68686a37
NR
973 } catch (IOException e) {
974 // woops, broken image
975 url = null;
976 }
08fe2e33
NR
977 }
978 }
979
980 return url;
981 }
982
373da363
NR
983 /**
984 * Open the input file that will be used through the support.
315f14ae
NR
985 * <p>
986 * Can return NULL, in which case you are supposed to work without an
987 * {@link InputStream}.
373da363
NR
988 *
989 * @param source
990 * the source {@link URL}
991 *
992 * @return the {@link InputStream}
993 *
994 * @throws IOException
995 * in case of I/O error
996 */
997 protected InputStream openInput(URL source) throws IOException {
998 return Instance.getCache().open(source, this, false);
999 }
1000
08fe2e33
NR
1001 /**
1002 * Reset then return {@link BasicSupport#in}.
1003 *
1004 * @return {@link BasicSupport#in}
08fe2e33 1005 */
68686a37
NR
1006 protected InputStream getInput() {
1007 return reset(in);
08fe2e33
NR
1008 }
1009
1010 /**
1011 * Fix the author name if it is prefixed with some "by" {@link String}.
1012 *
1013 * @param author
1014 * the author with a possible prefix
1015 *
1016 * @return the author without prefixes
1017 */
68686a37 1018 protected String fixAuthor(String author) {
08fe2e33
NR
1019 if (author != null) {
1020 for (String suffix : new String[] { " ", ":" }) {
1021 for (String byString : Instance.getConfig()
1022 .getString(Config.BYS).split(",")) {
1023 byString += suffix;
1024 if (author.toUpperCase().startsWith(byString.toUpperCase())) {
1025 author = author.substring(byString.length()).trim();
1026 }
1027 }
1028 }
1029
1030 // Special case (without suffix):
1031 if (author.startsWith("©")) {
1032 author = author.substring(1);
1033 }
1034 }
1035
1036 return author;
1037 }
1038
1039 /**
1040 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
1041 * and requotify them (i.e., separate them into QUOTE paragraphs and other
1042 * paragraphs (quotes or not)).
1043 *
1044 * @param para
a4143cd7 1045 * the paragraph to requotify (not necessarily a quote)
08fe2e33
NR
1046 *
1047 * @return the correctly (or so we hope) quotified paragraphs
1048 */
68e370a4 1049 protected List<Paragraph> requotify(Paragraph para) {
08fe2e33
NR
1050 List<Paragraph> newParas = new ArrayList<Paragraph>();
1051
68686a37
NR
1052 if (para.getType() == ParagraphType.QUOTE
1053 && para.getContent().length() > 2) {
08fe2e33
NR
1054 String line = para.getContent();
1055 boolean singleQ = line.startsWith("" + openQuote);
1056 boolean doubleQ = line.startsWith("" + openDoubleQuote);
1057
b4dc6ab5
NR
1058 // Do not try when more than one quote at a time
1059 // (some stories are not easily readable if we do)
1060 if (singleQ
1061 && line.indexOf(closeQuote, 1) < line
1062 .lastIndexOf(closeQuote)) {
1063 newParas.add(para);
1064 return newParas;
1065 }
1066 if (doubleQ
1067 && line.indexOf(closeDoubleQuote, 1) < line
1068 .lastIndexOf(closeDoubleQuote)) {
1069 newParas.add(para);
1070 return newParas;
1071 }
1072 //
1073
08fe2e33
NR
1074 if (!singleQ && !doubleQ) {
1075 line = openDoubleQuote + line + closeDoubleQuote;
793f1071
NR
1076 newParas.add(new Paragraph(ParagraphType.QUOTE, line, para
1077 .getWords()));
08fe2e33 1078 } else {
a6395bef 1079 char open = singleQ ? openQuote : openDoubleQuote;
08fe2e33 1080 char close = singleQ ? closeQuote : closeDoubleQuote;
a6395bef
NR
1081
1082 int posDot = -1;
1083 boolean inQuote = false;
1084 int i = 0;
1085 for (char car : line.toCharArray()) {
1086 if (car == open) {
1087 inQuote = true;
1088 } else if (car == close) {
1089 inQuote = false;
1090 } else if (car == '.' && !inQuote) {
1091 posDot = i;
1092 break;
1093 }
1094 i++;
08fe2e33
NR
1095 }
1096
1097 if (posDot >= 0) {
1098 String rest = line.substring(posDot + 1).trim();
1099 line = line.substring(0, posDot + 1).trim();
793f1071
NR
1100 long words = 1;
1101 for (char car : line.toCharArray()) {
1102 if (car == ' ') {
1103 words++;
1104 }
1105 }
1106 newParas.add(new Paragraph(ParagraphType.QUOTE, line, words));
68686a37
NR
1107 if (!rest.isEmpty()) {
1108 newParas.addAll(requotify(processPara(rest)));
1109 }
08fe2e33
NR
1110 } else {
1111 newParas.add(para);
1112 }
1113 }
1114 } else {
1115 newParas.add(para);
1116 }
1117
1118 return newParas;
1119 }
1120
1121 /**
1122 * Process a {@link Paragraph} from a raw line of text.
1123 * <p>
1124 * Will also fix quotes and HTML encoding if needed.
1125 *
1126 * @param line
1127 * the raw line
1128 *
1129 * @return the processed {@link Paragraph}
1130 */
22848428 1131 protected Paragraph processPara(String line) {
08fe2e33
NR
1132 line = ifUnhtml(line).trim();
1133
1134 boolean space = true;
1135 boolean brk = true;
1136 boolean quote = false;
1137 boolean tentativeCloseQuote = false;
1138 char prev = '\0';
1139 int dashCount = 0;
793f1071 1140 long words = 1;
08fe2e33
NR
1141
1142 StringBuilder builder = new StringBuilder();
1143 for (char car : line.toCharArray()) {
1144 if (car != '-') {
1145 if (dashCount > 0) {
1146 // dash, ndash and mdash: - – —
1147 // currently: always use mdash
1148 builder.append(dashCount == 1 ? '-' : '—');
1149 }
1150 dashCount = 0;
1151 }
1152
1153 if (tentativeCloseQuote) {
1154 tentativeCloseQuote = false;
22848428 1155 if (Character.isLetterOrDigit(car)) {
08fe2e33
NR
1156 builder.append("'");
1157 } else {
22848428
NR
1158 // handle double-single quotes as double quotes
1159 if (prev == car) {
1160 builder.append(closeDoubleQuote);
1161 continue;
22848428 1162 }
211f7ddb
NR
1163
1164 builder.append(closeQuote);
08fe2e33
NR
1165 }
1166 }
1167
1168 switch (car) {
1169 case ' ': // note: unbreakable space
1170 case ' ':
1171 case '\t':
1172 case '\n': // just in case
1173 case '\r': // just in case
793f1071
NR
1174 if (builder.length() > 0
1175 && builder.charAt(builder.length() - 1) != ' ') {
1176 words++;
1177 }
08fe2e33
NR
1178 builder.append(' ');
1179 break;
1180
1181 case '\'':
1182 if (space || (brk && quote)) {
1183 quote = true;
22848428
NR
1184 // handle double-single quotes as double quotes
1185 if (prev == car) {
1186 builder.deleteCharAt(builder.length() - 1);
1187 builder.append(openDoubleQuote);
1188 } else {
1189 builder.append(openQuote);
1190 }
1191 } else if (prev == ' ' || prev == car) {
1192 // handle double-single quotes as double quotes
1193 if (prev == car) {
1194 builder.deleteCharAt(builder.length() - 1);
1195 builder.append(openDoubleQuote);
1196 } else {
1197 builder.append(openQuote);
1198 }
08fe2e33
NR
1199 } else {
1200 // it is a quote ("I'm off") or a 'quote' ("This
1201 // 'good' restaurant"...)
1202 tentativeCloseQuote = true;
1203 }
1204 break;
1205
1206 case '"':
1207 if (space || (brk && quote)) {
1208 quote = true;
1209 builder.append(openDoubleQuote);
1210 } else if (prev == ' ') {
1211 builder.append(openDoubleQuote);
1212 } else {
1213 builder.append(closeDoubleQuote);
1214 }
1215 break;
1216
1217 case '-':
1218 if (space) {
1219 quote = true;
1220 } else {
1221 dashCount++;
1222 }
1223 space = false;
1224 break;
1225
1226 case '*':
1227 case '~':
1228 case '/':
1229 case '\\':
1230 case '<':
1231 case '>':
1232 case '=':
1233 case '+':
1234 case '_':
1235 case '–':
1236 case '—':
1237 space = false;
1238 builder.append(car);
1239 break;
1240
1241 case '‘':
1242 case '`':
1243 case '‹':
1244 case '﹁':
1245 case '〈':
1246 case '「':
1247 if (space || (brk && quote)) {
1248 quote = true;
1249 builder.append(openQuote);
1250 } else {
22848428
NR
1251 // handle double-single quotes as double quotes
1252 if (prev == car) {
1253 builder.deleteCharAt(builder.length() - 1);
1254 builder.append(openDoubleQuote);
1255 } else {
1256 builder.append(openQuote);
1257 }
08fe2e33
NR
1258 }
1259 space = false;
1260 brk = false;
1261 break;
1262
1263 case '’':
1264 case '›':
1265 case '﹂':
1266 case '〉':
1267 case '」':
1268 space = false;
1269 brk = false;
22848428
NR
1270 // handle double-single quotes as double quotes
1271 if (prev == car) {
1272 builder.deleteCharAt(builder.length() - 1);
1273 builder.append(closeDoubleQuote);
1274 } else {
1275 builder.append(closeQuote);
1276 }
08fe2e33
NR
1277 break;
1278
1279 case '«':
1280 case '“':
1281 case '﹃':
1282 case '《':
1283 case '『':
1284 if (space || (brk && quote)) {
1285 quote = true;
1286 builder.append(openDoubleQuote);
1287 } else {
1288 builder.append(openDoubleQuote);
1289 }
1290 space = false;
1291 brk = false;
1292 break;
1293
1294 case '»':
1295 case '”':
1296 case '﹄':
1297 case '》':
1298 case '』':
1299 space = false;
1300 brk = false;
1301 builder.append(closeDoubleQuote);
1302 break;
1303
1304 default:
1305 space = false;
1306 brk = false;
1307 builder.append(car);
1308 break;
1309 }
1310
1311 prev = car;
1312 }
1313
1314 if (tentativeCloseQuote) {
1315 tentativeCloseQuote = false;
1316 builder.append(closeQuote);
1317 }
1318
1319 line = builder.toString().trim();
1320
1321 ParagraphType type = ParagraphType.NORMAL;
1322 if (space) {
1323 type = ParagraphType.BLANK;
1324 } else if (brk) {
1325 type = ParagraphType.BREAK;
1326 } else if (quote) {
1327 type = ParagraphType.QUOTE;
1328 }
1329
793f1071 1330 return new Paragraph(type, line, words);
08fe2e33
NR
1331 }
1332
1333 /**
a4143cd7 1334 * Remove the HTML from the input <b>if</b> {@link BasicSupport#isHtml()} is
08fe2e33
NR
1335 * true.
1336 *
1337 * @param input
1338 * the input
1339 *
1340 * @return the no html version if needed
1341 */
1342 private String ifUnhtml(String input) {
1343 if (isHtml() && input != null) {
1344 return StringUtils.unhtml(input);
1345 }
1346
1347 return input;
1348 }
1349
1350 /**
1351 * Return a {@link BasicSupport} implementation supporting the given
1352 * resource if possible.
1353 *
1354 * @param url
1355 * the story resource
1356 *
1357 * @return an implementation that supports it, or NULL
1358 */
1359 public static BasicSupport getSupport(URL url) {
1360 if (url == null) {
1361 return null;
1362 }
1363
1364 // TEXT and INFO_TEXT always support files (not URLs though)
1365 for (SupportType type : SupportType.values()) {
1366 if (type != SupportType.TEXT && type != SupportType.INFO_TEXT) {
1367 BasicSupport support = getSupport(type);
1368 if (support != null && support.supports(url)) {
1369 return support;
1370 }
1371 }
1372 }
1373
373da363
NR
1374 for (SupportType type : new SupportType[] { SupportType.INFO_TEXT,
1375 SupportType.TEXT }) {
08fe2e33
NR
1376 BasicSupport support = getSupport(type);
1377 if (support != null && support.supports(url)) {
1378 return support;
1379 }
1380 }
1381
1382 return null;
1383 }
1384
1385 /**
1386 * Return a {@link BasicSupport} implementation supporting the given type.
1387 *
1388 * @param type
1389 * the type
1390 *
1391 * @return an implementation that supports it, or NULL
1392 */
1393 public static BasicSupport getSupport(SupportType type) {
1394 switch (type) {
1395 case EPUB:
1396 return new Epub().setType(type);
1397 case INFO_TEXT:
1398 return new InfoText().setType(type);
1399 case FIMFICTION:
315f14ae
NR
1400 try {
1401 // Can fail if no client key or NO in options
1402 return new FimfictionApi().setType(type);
1403 } catch (IOException e) {
1404 return new Fimfiction().setType(type);
1405 }
08fe2e33
NR
1406 case FANFICTION:
1407 return new Fanfiction().setType(type);
1408 case TEXT:
1409 return new Text().setType(type);
1410 case MANGAFOX:
1411 return new MangaFox().setType(type);
1412 case E621:
1413 return new E621().setType(type);
a4143cd7
NR
1414 case YIFFSTAR:
1415 return new YiffStar().setType(type);
f0608ab1
NR
1416 case E_HENTAI:
1417 return new EHentai().setType(type);
08fe2e33
NR
1418 case CBZ:
1419 return new Cbz().setType(type);
373da363
NR
1420 case HTML:
1421 return new Html().setType(type);
08fe2e33
NR
1422 }
1423
1424 return null;
1425 }
68686a37 1426
315f14ae
NR
1427 /**
1428 * Reset the given {@link InputStream} and return it.
1429 *
1430 * @param in
1431 * the {@link InputStream} to reset
1432 *
1433 * @return the same {@link InputStream} after reset
1434 */
1435 static protected InputStream reset(InputStream in) {
1436 try {
1437 if (in != null) {
1438 in.reset();
1439 }
1440 } catch (IOException e) {
1441 }
1442
1443 return in;
1444 }
1445
68686a37
NR
1446 /**
1447 * Return the first line from the given input which correspond to the given
1448 * selectors.
1449 *
1450 * @param in
1451 * the input
1452 * @param needle
1453 * a string that must be found inside the target line (also
1454 * supports "^" at start to say "only if it starts with" the
1455 * needle)
1456 * @param relativeLine
1457 * the line to return based upon the target line position (-1 =
1458 * the line before, 0 = the target line...)
1459 *
1460 * @return the line
1461 */
315f14ae
NR
1462 static protected String getLine(InputStream in, String needle,
1463 int relativeLine) {
68686a37
NR
1464 return getLine(in, needle, relativeLine, true);
1465 }
1466
1467 /**
1468 * Return a line from the given input which correspond to the given
1469 * selectors.
1470 *
1471 * @param in
1472 * the input
1473 * @param needle
1474 * a string that must be found inside the target line (also
1475 * supports "^" at start to say "only if it starts with" the
1476 * needle)
1477 * @param relativeLine
1478 * the line to return based upon the target line position (-1 =
1479 * the line before, 0 = the target line...)
1480 * @param first
1481 * takes the first result (as opposed to the last one, which will
1482 * also always spend the input)
1483 *
1484 * @return the line
1485 */
315f14ae
NR
1486 static protected String getLine(InputStream in, String needle,
1487 int relativeLine, boolean first) {
68686a37
NR
1488 String rep = null;
1489
315f14ae 1490 reset(in);
68686a37
NR
1491
1492 List<String> lines = new ArrayList<String>();
1493 @SuppressWarnings("resource")
1494 Scanner scan = new Scanner(in, "UTF-8");
1495 int index = -1;
1496 scan.useDelimiter("\\n");
1497 while (scan.hasNext()) {
1498 lines.add(scan.next());
1499
1500 if (index == -1) {
1501 if (needle.startsWith("^")) {
1502 if (lines.get(lines.size() - 1).startsWith(
1503 needle.substring(1))) {
1504 index = lines.size() - 1;
1505 }
1506
1507 } else {
1508 if (lines.get(lines.size() - 1).contains(needle)) {
1509 index = lines.size() - 1;
1510 }
1511 }
1512 }
1513
1514 if (index >= 0 && index + relativeLine < lines.size()) {
1515 rep = lines.get(index + relativeLine);
1516 if (first) {
1517 break;
1518 }
1519 }
1520 }
1521
1522 return rep;
1523 }
f0608ab1
NR
1524
1525 /**
1526 * Return the text between the key and the endKey (and optional subKey can
1527 * be passed, in this case we will look for the key first, then take the
1528 * text between the subKey and the endKey).
1529 * <p>
1530 * Will only match the first line with the given key if more than one are
1531 * possible. Which also means that if the subKey or endKey is not found on
1532 * that line, NULL will be returned.
1533 *
1534 * @param in
1535 * the input
1536 * @param key
27dc7179
NR
1537 * the key to match (also supports "^" at start to say
1538 * "only if it starts with" the key)
f0608ab1
NR
1539 * @param subKey
1540 * the sub key or NULL if none
1541 * @param endKey
1542 * the end key or NULL for "up to the end"
1543 * @return the text or NULL if not found
1544 */
315f14ae
NR
1545 static protected String getKeyLine(InputStream in, String key,
1546 String subKey, String endKey) {
1547 return getKeyText(getLine(in, key, 0), key, subKey, endKey);
1548 }
1549
1550 /**
1551 * Return the text between the key and the endKey (and optional subKey can
1552 * be passed, in this case we will look for the key first, then take the
1553 * text between the subKey and the endKey).
1554 *
1555 * @param in
1556 * the input
1557 * @param key
1558 * the key to match (also supports "^" at start to say
1559 * "only if it starts with" the key)
1560 * @param subKey
1561 * the sub key or NULL if none
1562 * @param endKey
1563 * the end key or NULL for "up to the end"
1564 * @return the text or NULL if not found
1565 */
1566 static protected String getKeyText(String in, String key, String subKey,
f0608ab1
NR
1567 String endKey) {
1568 String result = null;
1569
315f14ae 1570 String line = in;
f0608ab1
NR
1571 if (line != null && line.contains(key)) {
1572 line = line.substring(line.indexOf(key) + key.length());
1573 if (subKey == null || subKey.isEmpty() || line.contains(subKey)) {
1574 if (subKey != null) {
1575 line = line.substring(line.indexOf(subKey)
1576 + subKey.length());
1577 }
1578 if (endKey == null || line.contains(endKey)) {
1579 if (endKey != null) {
1580 line = line.substring(0, line.indexOf(endKey));
1581 result = line;
1582 }
1583 }
1584 }
1585 }
1586
1587 return result;
1588 }
315f14ae
NR
1589
1590 /**
1591 * Return the text between the key and the endKey (optional subKeys can be
1592 * passed, in this case we will look for the subKeys first, then take the
1593 * text between the key and the endKey).
1594 *
1595 * @param in
1596 * the input
1597 * @param key
1598 * the key to match
1599 * @param endKey
1600 * the end key or NULL for "up to the end"
1601 * @param afters
1602 * the sub-keys to find before checking for key/endKey
1603 *
1604 * @return the text or NULL if not found
1605 */
1606 static protected String getKeyTextAfter(String in, String key,
1607 String endKey, String... afters) {
1608
1609 if (in != null && !in.isEmpty()) {
1610 int pos = indexOfAfter(in, 0, afters);
1611 if (pos < 0) {
1612 return null;
1613 }
1614
1615 in = in.substring(pos);
1616 }
1617
1618 return getKeyText(in, key, null, endKey);
1619 }
1620
1621 /**
1622 * Return the first index after all the given "afters" have been found in
1623 * the {@link String}, or -1 if it was not possible.
1624 *
1625 * @param in
1626 * the input
1627 * @param startAt
1628 * start at this position in the string
1629 * @param afters
1630 * the sub-keys to find before checking for key/endKey
1631 *
1632 * @return the text or NULL if not found
1633 */
1634 static protected int indexOfAfter(String in, int startAt, String... afters) {
1635 int pos = -1;
1636 if (in != null && !in.isEmpty()) {
1637 pos = startAt;
1638 if (afters != null) {
1639 for (int i = 0; pos >= 0 && i < afters.length; i++) {
1640 String subKey = afters[i];
1641 if (!subKey.isEmpty()) {
1642 pos = in.indexOf(subKey, pos);
1643 if (pos >= 0) {
1644 pos += subKey.length();
1645 }
1646 }
1647 }
1648 }
1649 }
1650
1651 return pos;
1652 }
08fe2e33 1653}