1 package be
.nikiroo
.fanfix
.supported
;
3 import java
.io
.BufferedReader
;
4 import java
.io
.ByteArrayInputStream
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.io
.InputStreamReader
;
9 import java
.net
.MalformedURLException
;
11 import java
.util
.ArrayList
;
12 import java
.util
.Date
;
13 import java
.util
.List
;
14 import java
.util
.Map
.Entry
;
15 import java
.util
.Scanner
;
17 import be
.nikiroo
.fanfix
.Instance
;
18 import be
.nikiroo
.fanfix
.bundles
.Config
;
19 import be
.nikiroo
.fanfix
.bundles
.StringId
;
20 import be
.nikiroo
.fanfix
.data
.Chapter
;
21 import be
.nikiroo
.fanfix
.data
.MetaData
;
22 import be
.nikiroo
.fanfix
.data
.Paragraph
;
23 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
24 import be
.nikiroo
.fanfix
.data
.Story
;
25 import be
.nikiroo
.utils
.Image
;
26 import be
.nikiroo
.utils
.Progress
;
27 import be
.nikiroo
.utils
.StringUtils
;
30 * DEPRECATED: use the new Jsoup 'Node' system.
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.
36 * It will be used with 'resources' (usually web pages or files).
41 public abstract class BasicSupport_Deprecated
extends BasicSupport
{
42 private InputStream in
;
45 private char openQuote
= Instance
.getInstance().getTrans().getCharacter(StringId
.OPEN_SINGLE_QUOTE
);
46 private char closeQuote
= Instance
.getInstance().getTrans().getCharacter(StringId
.CLOSE_SINGLE_QUOTE
);
47 private char openDoubleQuote
= Instance
.getInstance().getTrans().getCharacter(StringId
.OPEN_DOUBLE_QUOTE
);
48 private char closeDoubleQuote
= Instance
.getInstance().getTrans().getCharacter(StringId
.CLOSE_DOUBLE_QUOTE
);
50 // New methods not used in Deprecated mode
52 protected String
getDesc() throws IOException
{
53 throw new RuntimeException("should not be used by legacy code");
57 protected MetaData
getMeta() throws IOException
{
58 throw new RuntimeException("should not be used by legacy code");
62 protected List
<Entry
<String
, URL
>> getChapters(Progress pg
)
64 throw new RuntimeException("should not be used by legacy code");
68 protected String
getChapterContent(URL chapUrl
, int number
, Progress pg
)
70 throw new RuntimeException("should not be used by legacy code");
74 public Story
process(Progress pg
) throws IOException
{
75 return process(getSource(), pg
);
81 * Return the {@link MetaData} of this story.
84 * the source of the story
86 * the input (the main resource)
88 * @return the associated {@link MetaData}, never NULL
91 * in case of I/O error
93 protected abstract MetaData
getMeta(URL source
, InputStream in
)
97 * Return the story description.
100 * the source of the story
102 * the input (the main resource)
104 * @return the description
106 * @throws IOException
107 * in case of I/O error
109 protected abstract String
getDesc(URL source
, InputStream in
)
113 * Return the list of chapters (name and resource).
116 * the source of the story
118 * the input (the main resource)
120 * the optional progress reporter
122 * @return the chapters
124 * @throws IOException
125 * in case of I/O error
127 protected abstract List
<Entry
<String
, URL
>> getChapters(URL source
,
128 InputStream in
, Progress pg
) throws IOException
;
131 * Return the content of the chapter (possibly HTML encoded, if
132 * {@link BasicSupport_Deprecated#isHtml()} is TRUE).
135 * the source of the story
137 * the input (the main resource)
141 * the optional progress reporter
143 * @return the content
145 * @throws IOException
146 * in case of I/O error
148 protected abstract String
getChapterContent(URL source
, InputStream in
,
149 int number
, Progress pg
) throws IOException
;
152 * Process the given story resource into a partially filled {@link Story}
153 * object containing the name and metadata, except for the description.
158 * @return the {@link Story}
160 * @throws IOException
161 * in case of I/O error
163 public Story
processMeta(URL url
) throws IOException
{
164 return processMeta(url
, true, false, null);
168 * Process the given story resource into a partially filled {@link Story}
169 * object containing the name and metadata.
174 * close "this" and "in" when done
176 * retrieve the description of the story, or not
178 * the optional progress reporter
180 * @return the {@link Story}, never NULL
182 * @throws IOException
183 * in case of I/O error
185 protected Story
processMeta(URL url
, boolean close
, boolean getDesc
,
186 Progress pg
) throws IOException
{
190 pg
.setMinMax(0, 100);
196 url
= getCanonicalUrl(url
);
198 setCurrentReferer(url
);
200 in
= openInput(url
); // NULL allowed here
202 preprocess(url
, getInput());
205 Story story
= new Story();
206 MetaData meta
= getMeta(url
, getInput());
207 if (meta
.getCreationDate() == null
208 || meta
.getCreationDate().trim().isEmpty()) {
209 meta
.setCreationDate(bsHelper
.formatDate(
210 StringUtils
.fromTime(new Date().getTime())));
213 pg
.put("meta", meta
);
217 if (meta
.getCover() == null) {
218 meta
.setCover(getDefaultCover(meta
.getSubject()));
224 String descChapterName
= Instance
.getInstance().getTrans().getString(StringId
.DESCRIPTION
);
225 story
.getMeta().setResume(makeChapter(url
, 0, descChapterName
, getDesc(url
, getInput()), null));
242 * Process the given story resource into a fully filled {@link Story}
248 * the optional progress reporter
250 * @return the {@link Story}, never NULL
252 * @throws IOException
253 * in case of I/O error
255 protected Story
process(URL url
, Progress pg
) throws IOException
{
259 pg
.setMinMax(0, 100);
262 url
= getCanonicalUrl(url
);
265 Progress pgMeta
= new Progress();
266 pg
.addProgress(pgMeta
, 10);
267 Story story
= processMeta(url
, false, true, pgMeta
);
268 pg
.put("meta", story
.getMeta());
269 if (!pgMeta
.isDone()) {
270 pgMeta
.setProgress(pgMeta
.getMax()); // 10%
273 setCurrentReferer(url
);
275 Progress pgGetChapters
= new Progress();
276 pg
.addProgress(pgGetChapters
, 10);
277 story
.setChapters(new ArrayList
<Chapter
>());
278 List
<Entry
<String
, URL
>> chapters
= getChapters(url
, getInput(),
280 if (!pgGetChapters
.isDone()) {
281 pgGetChapters
.setProgress(pgGetChapters
.getMax()); // 20%
284 if (chapters
!= null) {
285 Progress pgChaps
= new Progress("Extracting chapters", 0,
286 chapters
.size() * 300);
287 pg
.addProgress(pgChaps
, 80);
291 for (Entry
<String
, URL
> chap
: chapters
) {
292 pgChaps
.setName("Extracting chapter " + i
);
293 InputStream chapIn
= null;
294 if (chap
.getValue() != null) {
295 setCurrentReferer(chap
.getValue());
296 chapIn
= Instance
.getInstance().getCache().open(chap
.getValue(), this, false);
298 pgChaps
.setProgress(i
* 100);
300 Progress pgGetChapterContent
= new Progress();
301 Progress pgMakeChapter
= new Progress();
302 pgChaps
.addProgress(pgGetChapterContent
, 100);
303 pgChaps
.addProgress(pgMakeChapter
, 100);
305 String content
= getChapterContent(url
, chapIn
, i
,
306 pgGetChapterContent
);
307 if (!pgGetChapterContent
.isDone()) {
308 pgGetChapterContent
.setProgress(pgGetChapterContent
312 Chapter cc
= makeChapter(url
, i
, chap
.getKey(),
313 content
, pgMakeChapter
);
314 if (!pgMakeChapter
.isDone()) {
315 pgMakeChapter
.setProgress(pgMakeChapter
.getMax());
318 words
+= cc
.getWords();
319 story
.getChapters().add(cc
);
321 if (chapIn
!= null) {
329 story
.getMeta().setWords(words
);
331 pgChaps
.setName("Extracting chapters");
336 // Check for "no chapters" stories
337 if (story
.getChapters().isEmpty()
338 && story
.getMeta().getResume() != null
339 && !story
.getMeta().getResume().getParagraphs().isEmpty()) {
340 Chapter resume
= story
.getMeta().getResume();
343 story
.getChapters().add(resume
);
344 story
.getMeta().setWords(resume
.getWords());
346 String descChapterName
= Instance
.getInstance().getTrans()
347 .getString(StringId
.DESCRIPTION
);
348 resume
= new Chapter(0, descChapterName
);
349 story
.getMeta().setResume(resume
);
363 * Prepare the support if needed before processing.
366 * the source of the story
368 * the input (the main resource)
370 * @throws IOException
373 @SuppressWarnings("unused")
374 protected void preprocess(URL source
, InputStream in
) throws IOException
{
378 * Create a {@link Chapter} object from the given information, formatting
379 * the content as it should be.
382 * the source of the story
388 * the chapter content
390 * the optional progress reporter
392 * @return the {@link Chapter}, never NULL
394 * @throws IOException
395 * in case of I/O error
397 protected Chapter
makeChapter(URL source
, int number
, String name
,
398 String content
, Progress pg
) throws IOException
{
399 // Chapter name: process it correctly, then remove the possible
400 // redundant "Chapter x: " in front of it, or "-" (as in
401 // "Chapter 5: - Fun!" after the ": " was automatically added)
402 String chapterName
= processPara(name
).getContent().trim();
403 for (String lang
: Instance
.getInstance().getConfig().getList(Config
.CONF_CHAPTER
)) {
404 String chapterWord
= Instance
.getInstance().getConfig().getStringX(Config
.CONF_CHAPTER
, lang
);
405 if (chapterName
.startsWith(chapterWord
)) {
406 chapterName
= chapterName
.substring(chapterWord
.length())
412 if (chapterName
.startsWith(Integer
.toString(number
))) {
413 chapterName
= chapterName
.substring(
414 Integer
.toString(number
).length()).trim();
417 while (chapterName
.startsWith(":") || chapterName
.startsWith("-")) {
418 chapterName
= chapterName
.substring(1).trim();
422 Chapter chap
= new Chapter(number
, chapterName
);
424 if (content
!= null) {
425 List
<Paragraph
> paras
= makeParagraphs(source
, content
, pg
);
427 for (Paragraph para
: paras
) {
428 words
+= para
.getWords();
430 chap
.setParagraphs(paras
);
431 chap
.setWords(words
);
439 * Convert the given content into {@link Paragraph}s.
442 * the source URL of the story
444 * the textual content
446 * the optional progress reporter
448 * @return the {@link Paragraph}s (can be empty, but never NULL)
450 * @throws IOException
451 * in case of I/O error
453 protected List
<Paragraph
> makeParagraphs(URL source
, String content
,
454 Progress pg
) throws IOException
{
460 // Special <HR> processing:
461 content
= content
.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
465 List
<Paragraph
> paras
= new ArrayList
<Paragraph
>();
466 if (content
!= null && !content
.trim().isEmpty()) {
468 String
[] tab
= content
.split("(<p>|</p>|<br>|<br/>)");
469 pg
.setMinMax(0, tab
.length
);
471 for (String line
: tab
) {
472 if (line
.startsWith("[") && line
.endsWith("]")) {
473 pg
.setName("Extracting image " + i
);
475 paras
.add(makeParagraph(source
, line
.trim()));
480 List
<String
> lines
= new ArrayList
<String
>();
481 BufferedReader buff
= null;
483 buff
= new BufferedReader(
484 new InputStreamReader(new ByteArrayInputStream(
485 content
.getBytes("UTF-8")), "UTF-8"));
486 for (String line
= buff
.readLine(); line
!= null; line
= buff
488 lines
.add(line
.trim());
496 pg
.setMinMax(0, lines
.size());
498 for (String line
: lines
) {
499 if (line
.startsWith("[") && line
.endsWith("]")) {
500 pg
.setName("Extracting image " + i
);
502 paras
.add(makeParagraph(source
, line
));
508 // Check quotes for "bad" format
509 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
510 for (Paragraph para
: paras
) {
511 newParas
.addAll(requotify(para
));
515 // Remove double blanks/brks
516 fixBlanksBreaks(paras
);
523 * Convert the given line into a single {@link Paragraph}.
526 * the source URL of the story
528 * the textual content of the paragraph
530 * @return the {@link Paragraph}, never NULL
532 private Paragraph
makeParagraph(URL source
, String line
) {
534 if (line
.startsWith("[") && line
.endsWith("]")) {
535 image
= getImage(this, source
, line
.substring(1, line
.length() - 1)
540 return new Paragraph(image
);
543 return processPara(line
);
547 * Fix the {@link ParagraphType#BLANK}s and {@link ParagraphType#BREAK}s of
548 * those {@link Paragraph}s.
550 * The resulting list will not contain a starting or trailing blank/break
551 * nor 2 blanks or breaks following each other.
554 * the list of {@link Paragraph}s to fix
556 protected void fixBlanksBreaks(List
<Paragraph
> paras
) {
557 boolean space
= false;
559 for (int i
= 0; i
< paras
.size(); i
++) {
560 Paragraph para
= paras
.get(i
);
561 boolean thisSpace
= para
.getType() == ParagraphType
.BLANK
;
562 boolean thisBrk
= para
.getType() == ParagraphType
.BREAK
;
564 if (i
> 0 && space
&& thisBrk
) {
567 } else if ((space
|| brk
) && (thisSpace
|| thisBrk
)) {
576 // Remove blank/brk at start
578 && (paras
.get(0).getType() == ParagraphType
.BLANK
|| paras
.get(
579 0).getType() == ParagraphType
.BREAK
)) {
583 // Remove blank/brk at end
584 int last
= paras
.size() - 1;
586 && (paras
.get(last
).getType() == ParagraphType
.BLANK
|| paras
587 .get(last
).getType() == ParagraphType
.BREAK
)) {
593 * Get the default cover related to this subject (see <tt>.info</tt> files).
598 * @return the cover if any, or NULL
600 static Image
getDefaultCover(String subject
) {
601 if (subject
!= null && !subject
.isEmpty() && Instance
.getInstance().getCoverDir() != null) {
603 File fileCover
= new File(Instance
.getInstance().getCoverDir(), subject
);
604 return getImage(null, fileCover
.toURI().toURL(), subject
);
605 } catch (MalformedURLException e
) {
613 * Return the list of supported image extensions.
615 * @param emptyAllowed
616 * TRUE to allow an empty extension on first place, which can be
617 * used when you may already have an extension in your input but
618 * are not sure about it
620 * @return the extensions
622 static String
[] getImageExt(boolean emptyAllowed
) {
624 return new String
[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
627 return new String
[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
631 * Check if the given resource can be a local image or a remote image, then
632 * refresh the cache with it if it is.
637 * the resource to check
639 * @return the image if found, or NULL
642 static Image
getImage(BasicSupport_Deprecated support
, URL source
,
644 URL url
= getImageUrl(support
, source
, line
);
646 if ("file".equals(url
.getProtocol())) {
647 if (new File(url
.getPath()).isDirectory()) {
651 InputStream in
= null;
653 in
= Instance
.getInstance().getCache().open(url
, getSupport(url
), true);
654 Image img
= new Image(in
);
655 if (img
.getSize() == 0) {
657 throw new IOException(
658 "Empty image not accepted");
661 } catch (IOException e
) {
666 } catch (IOException e
) {
676 * Check if the given resource can be a local image or a remote image, then
677 * refresh the cache with it if it is.
682 * the resource to check
684 * @return the image URL if found, or NULL
687 static URL
getImageUrl(BasicSupport_Deprecated support
, URL source
,
693 if (source
!= null) {
695 String relPath
= null;
696 String absPath
= null;
698 String path
= new File(source
.getFile()).getParent();
699 relPath
= new File(new File(path
), line
.trim())
701 } catch (Exception e
) {
702 // Cannot be converted to path (one possibility to take
703 // into account: absolute path on Windows)
706 absPath
= new File(line
.trim()).getAbsolutePath();
707 } catch (Exception e
) {
708 // Cannot be converted to path (at all)
711 for (String ext
: getImageExt(true)) {
712 File absFile
= new File(absPath
+ ext
);
713 File relFile
= new File(relPath
+ ext
);
714 if (absPath
!= null && absFile
.exists()
715 && absFile
.isFile()) {
716 url
= absFile
.toURI().toURL();
717 } else if (relPath
!= null && relFile
.exists()
718 && relFile
.isFile()) {
719 url
= relFile
.toURI().toURL();
722 } catch (Exception e
) {
723 // Should not happen since we control the correct arguments
730 for (String ext
: getImageExt(true)) {
731 if (Instance
.getInstance().getCache().check(new URL(line
+ ext
), true)) {
732 url
= new URL(line
+ ext
);
739 for (String ext
: getImageExt(true)) {
741 url
= new URL(line
+ ext
);
742 Instance
.getInstance().getCache().refresh(url
, support
, true);
744 } catch (IOException e
) {
745 // no image with this ext
750 } catch (MalformedURLException e
) {
755 // refresh the cached file
758 Instance
.getInstance().getCache().refresh(url
, support
, true);
759 } catch (IOException e
) {
760 // woops, broken image
770 * Open the input file that will be used through the support.
772 * Can return NULL, in which case you are supposed to work without an
773 * {@link InputStream}.
776 * the source {@link URL}
778 * @return the {@link InputStream}
780 * @throws IOException
781 * in case of I/O error
783 protected InputStream
openInput(URL source
) throws IOException
{
784 return Instance
.getInstance().getCache().open(source
, this, false);
788 * Reset then return {@link BasicSupport_Deprecated#in}.
790 * @return {@link BasicSupport_Deprecated#in}
792 protected InputStream
getInput() {
797 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
798 * and requotify them (i.e., separate them into QUOTE paragraphs and other
799 * paragraphs (quotes or not)).
802 * the paragraph to requotify (not necessarily a quote)
804 * @return the correctly (or so we hope) quotified paragraphs
806 protected List
<Paragraph
> requotify(Paragraph para
) {
807 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
809 if (para
.getType() == ParagraphType
.QUOTE
810 && para
.getContent().length() > 2) {
811 String line
= para
.getContent();
812 boolean singleQ
= line
.startsWith("" + openQuote
);
813 boolean doubleQ
= line
.startsWith("" + openDoubleQuote
);
815 // Do not try when more than one quote at a time
816 // (some stories are not easily readable if we do)
818 && line
.indexOf(closeQuote
, 1) < line
819 .lastIndexOf(closeQuote
)) {
824 && line
.indexOf(closeDoubleQuote
, 1) < line
825 .lastIndexOf(closeDoubleQuote
)) {
831 if (!singleQ
&& !doubleQ
) {
832 line
= openDoubleQuote
+ line
+ closeDoubleQuote
;
833 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
, para
836 char open
= singleQ ? openQuote
: openDoubleQuote
;
837 char close
= singleQ ? closeQuote
: closeDoubleQuote
;
840 boolean inQuote
= false;
842 for (char car
: line
.toCharArray()) {
845 } else if (car
== close
) {
847 } else if (car
== '.' && !inQuote
) {
855 String rest
= line
.substring(posDot
+ 1).trim();
856 line
= line
.substring(0, posDot
+ 1).trim();
858 for (char car
: line
.toCharArray()) {
863 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
, words
));
864 if (!rest
.isEmpty()) {
865 newParas
.addAll(requotify(processPara(rest
)));
879 * Process a {@link Paragraph} from a raw line of text.
881 * Will also fix quotes and HTML encoding if needed.
886 * @return the processed {@link Paragraph}, never NULL
888 protected Paragraph
processPara(String line
) {
889 line
= ifUnhtml(line
).trim();
891 boolean space
= true;
893 boolean quote
= false;
894 boolean tentativeCloseQuote
= false;
899 StringBuilder builder
= new StringBuilder();
900 for (char car
: line
.toCharArray()) {
903 // dash, ndash and mdash: - – —
904 // currently: always use mdash
905 builder
.append(dashCount
== 1 ?
'-' : '—');
910 if (tentativeCloseQuote
) {
911 tentativeCloseQuote
= false;
912 if (Character
.isLetterOrDigit(car
)) {
915 // handle double-single quotes as double quotes
917 builder
.append(closeDoubleQuote
);
921 builder
.append(closeQuote
);
926 case ' ': // note: unbreakable space
929 case '\n': // just in case
930 case '\r': // just in case
931 if (builder
.length() > 0
932 && builder
.charAt(builder
.length() - 1) != ' ') {
939 if (space
|| (brk
&& quote
)) {
941 // handle double-single quotes as double quotes
943 builder
.deleteCharAt(builder
.length() - 1);
944 builder
.append(openDoubleQuote
);
946 builder
.append(openQuote
);
948 } else if (prev
== ' ' || prev
== car
) {
949 // handle double-single quotes as double quotes
951 builder
.deleteCharAt(builder
.length() - 1);
952 builder
.append(openDoubleQuote
);
954 builder
.append(openQuote
);
957 // it is a quote ("I'm off") or a 'quote' ("This
958 // 'good' restaurant"...)
959 tentativeCloseQuote
= true;
964 if (space
|| (brk
&& quote
)) {
966 builder
.append(openDoubleQuote
);
967 } else if (prev
== ' ') {
968 builder
.append(openDoubleQuote
);
970 builder
.append(closeDoubleQuote
);
1004 if (space
|| (brk
&& quote
)) {
1006 builder
.append(openQuote
);
1008 // handle double-single quotes as double quotes
1010 builder
.deleteCharAt(builder
.length() - 1);
1011 builder
.append(openDoubleQuote
);
1013 builder
.append(openQuote
);
1027 // handle double-single quotes as double quotes
1029 builder
.deleteCharAt(builder
.length() - 1);
1030 builder
.append(closeDoubleQuote
);
1032 builder
.append(closeQuote
);
1041 if (space
|| (brk
&& quote
)) {
1043 builder
.append(openDoubleQuote
);
1045 builder
.append(openDoubleQuote
);
1058 builder
.append(closeDoubleQuote
);
1064 builder
.append(car
);
1071 if (tentativeCloseQuote
) {
1072 tentativeCloseQuote
= false;
1073 builder
.append(closeQuote
);
1076 line
= builder
.toString().trim();
1078 ParagraphType type
= ParagraphType
.NORMAL
;
1080 type
= ParagraphType
.BLANK
;
1082 type
= ParagraphType
.BREAK
;
1084 type
= ParagraphType
.QUOTE
;
1087 return new Paragraph(type
, line
, words
);
1091 * Remove the HTML from the input <b>if</b>
1092 * {@link BasicSupport_Deprecated#isHtml()} is true.
1097 * @return the no html version if needed
1099 private String
ifUnhtml(String input
) {
1100 if (isHtml() && input
!= null) {
1101 return StringUtils
.unhtml(input
);
1108 * Reset the given {@link InputStream} and return it.
1111 * the {@link InputStream} to reset
1113 * @return the same {@link InputStream} after reset
1115 static protected InputStream
reset(InputStream in
) {
1120 } catch (IOException e
) {
1127 * Return the first line from the given input which correspond to the given
1133 * a string that must be found inside the target line (also
1134 * supports "^" at start to say "only if it starts with" the
1136 * @param relativeLine
1137 * the line to return based upon the target line position (-1 =
1138 * the line before, 0 = the target line...)
1140 * @return the line, or NULL if not found
1142 static protected String
getLine(InputStream in
, String needle
,
1144 return getLine(in
, needle
, relativeLine
, true);
1148 * Return a line from the given input which correspond to the given
1154 * a string that must be found inside the target line (also
1155 * supports "^" at start to say "only if it starts with" the
1157 * @param relativeLine
1158 * the line to return based upon the target line position (-1 =
1159 * the line before, 0 = the target line...)
1161 * takes the first result (as opposed to the last one, which will
1162 * also always spend the input)
1164 * @return the line, or NULL if not found
1166 static protected String
getLine(InputStream in
, String needle
,
1167 int relativeLine
, boolean first
) {
1172 List
<String
> lines
= new ArrayList
<String
>();
1173 @SuppressWarnings("resource")
1174 Scanner scan
= new Scanner(in
, "UTF-8");
1176 scan
.useDelimiter("\\n");
1177 while (scan
.hasNext()) {
1178 lines
.add(scan
.next());
1181 if (needle
.startsWith("^")) {
1182 if (lines
.get(lines
.size() - 1).startsWith(
1183 needle
.substring(1))) {
1184 index
= lines
.size() - 1;
1188 if (lines
.get(lines
.size() - 1).contains(needle
)) {
1189 index
= lines
.size() - 1;
1194 if (index
>= 0 && index
+ relativeLine
< lines
.size()) {
1195 rep
= lines
.get(index
+ relativeLine
);
1206 * Return the text between the key and the endKey (and optional subKey can
1207 * be passed, in this case we will look for the key first, then take the
1208 * text between the subKey and the endKey).
1210 * Will only match the first line with the given key if more than one are
1211 * possible. Which also means that if the subKey or endKey is not found on
1212 * that line, NULL will be returned.
1217 * the key to match (also supports "^" at start to say
1218 * "only if it starts with" the key)
1220 * the sub key or NULL if none
1222 * the end key or NULL for "up to the end"
1223 * @return the text or NULL if not found
1225 static protected String
getKeyLine(InputStream in
, String key
,
1226 String subKey
, String endKey
) {
1227 return getKeyText(getLine(in
, key
, 0), key
, subKey
, endKey
);
1231 * Return the text between the key and the endKey (and optional subKey can
1232 * be passed, in this case we will look for the key first, then take the
1233 * text between the subKey and the endKey).
1238 * the key to match (also supports "^" at start to say
1239 * "only if it starts with" the key)
1241 * the sub key or NULL if none
1243 * the end key or NULL for "up to the end"
1244 * @return the text or NULL if not found
1246 static protected String
getKeyText(String in
, String key
, String subKey
,
1248 String result
= null;
1251 if (line
!= null && line
.contains(key
)) {
1252 line
= line
.substring(line
.indexOf(key
) + key
.length());
1253 if (subKey
== null || subKey
.isEmpty() || line
.contains(subKey
)) {
1254 if (subKey
!= null) {
1255 line
= line
.substring(line
.indexOf(subKey
)
1258 if (endKey
== null || line
.contains(endKey
)) {
1259 if (endKey
!= null) {
1260 line
= line
.substring(0, line
.indexOf(endKey
));
1271 * Return the text between the key and the endKey (optional subKeys can be
1272 * passed, in this case we will look for the subKeys first, then take the
1273 * text between the key and the endKey).
1280 * the end key or NULL for "up to the end"
1282 * the sub-keys to find before checking for key/endKey
1284 * @return the text or NULL if not found
1286 static protected String
getKeyTextAfter(String in
, String key
,
1287 String endKey
, String
... afters
) {
1289 if (in
!= null && !in
.isEmpty()) {
1290 int pos
= indexOfAfter(in
, 0, afters
);
1295 in
= in
.substring(pos
);
1298 return getKeyText(in
, key
, null, endKey
);
1302 * Return the first index after all the given "afters" have been found in
1303 * the {@link String}, or -1 if it was not possible.
1308 * start at this position in the string
1310 * the sub-keys to find before checking for key/endKey
1312 * @return the text or NULL if not found
1314 static protected int indexOfAfter(String in
, int startAt
, String
... afters
) {
1316 if (in
!= null && !in
.isEmpty()) {
1318 if (afters
!= null) {
1319 for (int i
= 0; pos
>= 0 && i
< afters
.length
; i
++) {
1320 String subKey
= afters
[i
];
1321 if (!subKey
.isEmpty()) {
1322 pos
= in
.indexOf(subKey
, pos
);
1324 pos
+= subKey
.length();