1 package be
.nikiroo
.fanfix
.supported
;
3 import java
.awt
.image
.BufferedImage
;
4 import java
.io
.BufferedReader
;
5 import java
.io
.ByteArrayInputStream
;
7 import java
.io
.IOException
;
8 import java
.io
.InputStream
;
9 import java
.io
.InputStreamReader
;
10 import java
.net
.MalformedURLException
;
12 import java
.util
.ArrayList
;
13 import java
.util
.Date
;
14 import java
.util
.HashMap
;
15 import java
.util
.List
;
17 import java
.util
.Map
.Entry
;
18 import java
.util
.Scanner
;
20 import be
.nikiroo
.fanfix
.Instance
;
21 import be
.nikiroo
.fanfix
.bundles
.Config
;
22 import be
.nikiroo
.fanfix
.bundles
.StringId
;
23 import be
.nikiroo
.fanfix
.data
.Chapter
;
24 import be
.nikiroo
.fanfix
.data
.MetaData
;
25 import be
.nikiroo
.fanfix
.data
.Paragraph
;
26 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
27 import be
.nikiroo
.fanfix
.data
.Story
;
28 import be
.nikiroo
.utils
.IOUtils
;
29 import be
.nikiroo
.utils
.Progress
;
30 import be
.nikiroo
.utils
.StringUtils
;
33 * This class is the base class used by the other support classes. It can be
34 * used outside of this package, and have static method that you can use to get
35 * access to the correct support class.
37 * It will be used with 'resources' (usually web pages or files).
41 public abstract class BasicSupport
{
43 * The supported input types for which we can get a {@link BasicSupport}
48 public enum SupportType
{
49 /** EPUB files created with this program */
51 /** Pure text file with some rules */
53 /** TEXT but with associated .info file */
55 /** My Little Pony fanfictions */
57 /** Fanfictions from a lot of different universes */
59 /** Website with lots of Mangas */
61 /** Furry website with comics support */
63 /** Furry website with stories */
71 * A description of this support type (more information than the
72 * {@link BasicSupport#getSourceName()}).
74 * @return the description
76 public String
getDesc() {
77 String desc
= Instance
.getTrans().getStringX(StringId
.INPUT_DESC
,
81 desc
= Instance
.getTrans().getString(StringId
.INPUT_DESC
, this);
88 * The name of this support type (a short version).
92 public String
getSourceName() {
93 BasicSupport support
= BasicSupport
.getSupport(this);
94 if (support
!= null) {
95 return support
.getSourceName();
102 public String
toString() {
103 return super.toString().toLowerCase();
107 * Call {@link SupportType#valueOf(String.toUpperCase())}.
110 * the possible type name
112 * @return NULL or the type
114 public static SupportType
valueOfUC(String typeName
) {
115 return SupportType
.valueOf(typeName
== null ?
null : typeName
120 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
121 * NULL for NULL instead of raising exception.
124 * the possible type name
126 * @return NULL or the type
128 public static SupportType
valueOfNullOkUC(String typeName
) {
129 if (typeName
== null) {
133 return SupportType
.valueOfUC(typeName
);
137 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
138 * NULL in case of error instead of raising an exception.
141 * the possible type name
143 * @return NULL or the type
145 public static SupportType
valueOfAllOkUC(String typeName
) {
147 return SupportType
.valueOfUC(typeName
);
148 } catch (Exception e
) {
154 private InputStream in
;
155 private SupportType type
;
156 private URL currentReferer
; // with only one 'r', as in 'HTTP'...
159 private char openQuote
= Instance
.getTrans().getChar(
160 StringId
.OPEN_SINGLE_QUOTE
);
161 private char closeQuote
= Instance
.getTrans().getChar(
162 StringId
.CLOSE_SINGLE_QUOTE
);
163 private char openDoubleQuote
= Instance
.getTrans().getChar(
164 StringId
.OPEN_DOUBLE_QUOTE
);
165 private char closeDoubleQuote
= Instance
.getTrans().getChar(
166 StringId
.CLOSE_DOUBLE_QUOTE
);
169 * The name of this support class.
173 protected abstract String
getSourceName();
176 * Check if the given resource is supported by this {@link BasicSupport}.
179 * the resource to check for
181 * @return TRUE if it is
183 protected abstract boolean supports(URL url
);
186 * Return TRUE if the support will return HTML encoded content values for
187 * the chapters content.
189 * @return TRUE for HTML
191 protected abstract boolean isHtml();
193 protected abstract MetaData
getMeta(URL source
, InputStream in
)
197 * Return the story description.
200 * the source of the story
202 * the input (the main resource)
204 * @return the description
206 * @throws IOException
207 * in case of I/O error
209 protected abstract String
getDesc(URL source
, InputStream in
)
213 * Return the list of chapters (name and resource).
216 * the source of the story
218 * the input (the main resource)
220 * @return the chapters
222 * @throws IOException
223 * in case of I/O error
225 protected abstract List
<Entry
<String
, URL
>> getChapters(URL source
,
226 InputStream in
) throws IOException
;
229 * Return the content of the chapter (possibly HTML encoded, if
230 * {@link BasicSupport#isHtml()} is TRUE).
233 * the source of the story
235 * the input (the main resource)
239 * @return the content
241 * @throws IOException
242 * in case of I/O error
244 protected abstract String
getChapterContent(URL source
, InputStream in
,
245 int number
) throws IOException
;
248 * Log into the support (can be a no-op depending upon the support).
250 * @throws IOException
251 * in case of I/O error
253 public void login() throws IOException
{
258 * Return the list of cookies (values included) that must be used to
259 * correctly fetch the resources.
261 * You are expected to call the super method implementation if you override
264 * @return the cookies
266 * @throws IOException
267 * in case of I/O error
269 public Map
<String
, String
> getCookies() throws IOException
{
270 return new HashMap
<String
, String
>();
274 * Return the canonical form of the main {@link URL}.
277 * the source {@link URL}
279 * @return the canonical form of this {@link URL}
281 * @throws IOException
282 * in case of I/O error
284 public URL
getCanonicalUrl(URL source
) throws IOException
{
289 * Process the given story resource into a partially filled {@link Story}
290 * object containing the name and metadata, except for the description.
295 * @return the {@link Story}
297 * @throws IOException
298 * in case of I/O error
300 public Story
processMeta(URL url
) throws IOException
{
301 return processMeta(url
, true, false);
305 * Process the given story resource into a partially filled {@link Story}
306 * object containing the name and metadata.
312 * close "this" and "in" when done
314 * @return the {@link Story}
316 * @throws IOException
317 * in case of I/O error
319 protected Story
processMeta(URL url
, boolean close
, boolean getDesc
)
323 url
= getCanonicalUrl(url
);
325 setCurrentReferer(url
);
333 preprocess(url
, getInput());
335 Story story
= new Story();
336 MetaData meta
= getMeta(url
, getInput());
337 if (meta
.getCreationDate() == null
338 || meta
.getCreationDate().isEmpty()) {
339 meta
.setCreationDate(StringUtils
.fromTime(new Date().getTime()));
343 if (meta
!= null && meta
.getCover() == null) {
344 meta
.setCover(getDefaultCover(meta
.getSubject()));
348 String descChapterName
= Instance
.getTrans().getString(
349 StringId
.DESCRIPTION
);
350 story
.getMeta().setResume(
351 makeChapter(url
, 0, descChapterName
,
352 getDesc(url
, getInput())));
360 } catch (IOException e
) {
369 setCurrentReferer(null);
374 * Process the given story resource into a fully filled {@link Story}
380 * the optional progress reporter
382 * @return the {@link Story}
384 * @throws IOException
385 * in case of I/O error
387 public Story
process(URL url
, Progress pg
) throws IOException
{
391 pg
.setMinMax(0, 100);
394 url
= getCanonicalUrl(url
);
397 Story story
= processMeta(url
, false, true);
404 pg
.setName("Retrieving " + story
.getMeta().getTitle());
406 setCurrentReferer(url
);
408 story
.setChapters(new ArrayList
<Chapter
>());
410 List
<Entry
<String
, URL
>> chapters
= getChapters(url
, getInput());
414 if (chapters
!= null) {
415 Progress pgChaps
= new Progress(0, chapters
.size());
416 pg
.addProgress(pgChaps
, 80);
419 for (Entry
<String
, URL
> chap
: chapters
) {
420 setCurrentReferer(chap
.getValue());
421 InputStream chapIn
= Instance
.getCache().open(
422 chap
.getValue(), this, true);
424 Chapter cc
= makeChapter(url
, i
, chap
.getKey(),
425 getChapterContent(url
, chapIn
, i
));
426 words
+= cc
.getWords();
427 story
.getChapters().add(cc
);
428 if (story
.getMeta() != null) {
429 story
.getMeta().setWords(words
);
435 pgChaps
.setProgress(i
++);
446 } catch (IOException e
) {
454 setCurrentReferer(null);
463 public SupportType
getType() {
468 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
469 * the current {@link URL} we work on.
471 * @return the referer
473 public URL
getCurrentReferer() {
474 return currentReferer
;
478 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
479 * the current {@link URL} we work on.
481 * @param currentReferer
484 protected void setCurrentReferer(URL currentReferer
) {
485 this.currentReferer
= currentReferer
;
496 protected BasicSupport
setType(SupportType type
) {
502 * Prepare the support if needed before processing.
505 * the source of the story
507 * the input (the main resource)
509 * @throws IOException
512 protected void preprocess(URL source
, InputStream in
) throws IOException
{
516 * Now that we have processed the {@link Story}, close the resources if any.
518 * @throws IOException
521 protected void close() throws IOException
{
525 * Create a {@link Chapter} object from the given information, formatting
526 * the content as it should be.
533 * the chapter content
535 * @return the {@link Chapter}
537 * @throws IOException
538 * in case of I/O error
540 protected Chapter
makeChapter(URL source
, int number
, String name
,
541 String content
) throws IOException
{
542 // Chapter name: process it correctly, then remove the possible
543 // redundant "Chapter x: " in front of it
544 String chapterName
= processPara(name
).getContent().trim();
545 for (String lang
: Instance
.getConfig().getString(Config
.CHAPTER
)
547 String chapterWord
= Instance
.getConfig().getStringX(
548 Config
.CHAPTER
, lang
);
549 if (chapterName
.startsWith(chapterWord
)) {
550 chapterName
= chapterName
.substring(chapterWord
.length())
556 if (chapterName
.startsWith(Integer
.toString(number
))) {
557 chapterName
= chapterName
.substring(
558 Integer
.toString(number
).length()).trim();
561 if (chapterName
.startsWith(":")) {
562 chapterName
= chapterName
.substring(1).trim();
566 Chapter chap
= new Chapter(number
, chapterName
);
568 if (content
!= null) {
569 List
<Paragraph
> paras
= makeParagraphs(source
, content
);
571 for (Paragraph para
: paras
) {
572 words
+= para
.getWords();
574 chap
.setParagraphs(paras
);
575 chap
.setWords(words
);
583 * Convert the given content into {@link Paragraph}s.
586 * the source URL of the story
588 * the textual content
590 * @return the {@link Paragraph}s
592 * @throws IOException
593 * in case of I/O error
595 protected List
<Paragraph
> makeParagraphs(URL source
, String content
)
598 // Special <HR> processing:
599 content
= content
.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
603 List
<Paragraph
> paras
= new ArrayList
<Paragraph
>();
604 InputStream in
= new ByteArrayInputStream(content
.getBytes("UTF-8"));
606 BufferedReader buff
= new BufferedReader(new InputStreamReader(in
,
609 for (String encodedLine
= buff
.readLine(); encodedLine
!= null; encodedLine
= buff
613 lines
= encodedLine
.split("(<p>|</p>|<br>|<br/>|\\n)");
615 lines
= new String
[] { encodedLine
};
618 for (String aline
: lines
) {
619 String line
= aline
.trim();
622 if (line
.startsWith("[") && line
.endsWith("]")) {
623 image
= getImageUrl(this, source
,
624 line
.substring(1, line
.length() - 1).trim());
628 paras
.add(new Paragraph(image
));
630 paras
.add(processPara(line
));
638 // Check quotes for "bad" format
639 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
640 for (Paragraph para
: paras
) {
641 newParas
.addAll(requotify(para
));
645 // Remove double blanks/brks
646 fixBlanksBreaks(paras
);
652 * Fix the {@link ParagraphType#BLANK}s and {@link ParagraphType#BREAK}s of
653 * those {@link Paragraph}s.
655 * The resulting list will not contain a starting or trailing blank/break
656 * nor 2 blanks or breaks following each other.
659 * the list of {@link Paragraph}s to fix
661 protected void fixBlanksBreaks(List
<Paragraph
> paras
) {
662 boolean space
= false;
664 for (int i
= 0; i
< paras
.size(); i
++) {
665 Paragraph para
= paras
.get(i
);
666 boolean thisSpace
= para
.getType() == ParagraphType
.BLANK
;
667 boolean thisBrk
= para
.getType() == ParagraphType
.BREAK
;
669 if (i
> 0 && space
&& thisBrk
) {
672 } else if ((space
|| brk
) && (thisSpace
|| thisBrk
)) {
681 // Remove blank/brk at start
683 && (paras
.get(0).getType() == ParagraphType
.BLANK
|| paras
.get(
684 0).getType() == ParagraphType
.BREAK
)) {
688 // Remove blank/brk at end
689 int last
= paras
.size() - 1;
691 && (paras
.get(last
).getType() == ParagraphType
.BLANK
|| paras
692 .get(last
).getType() == ParagraphType
.BREAK
)) {
698 * Get the default cover related to this subject (see <tt>.info</tt> files).
703 * @return the cover if any, or NULL
705 static BufferedImage
getDefaultCover(String subject
) {
706 if (subject
!= null && !subject
.isEmpty()
707 && Instance
.getCoverDir() != null) {
709 File fileCover
= new File(Instance
.getCoverDir(), subject
);
710 return getImage(null, fileCover
.toURI().toURL(), subject
);
711 } catch (MalformedURLException e
) {
719 * Return the list of supported image extensions.
721 * @param emptyAllowed
722 * TRUE to allow an empty extension on first place, which can be
723 * used when you may already have an extension in your input but
724 * are not sure about it
726 * @return the extensions
728 static String
[] getImageExt(boolean emptyAllowed
) {
730 return new String
[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
732 return new String
[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
737 * Check if the given resource can be a local image or a remote image, then
738 * refresh the cache with it if it is.
743 * the resource to check
745 * @return the image if found, or NULL
748 static BufferedImage
getImage(BasicSupport support
, URL source
, String line
) {
749 URL url
= getImageUrl(support
, source
, line
);
751 InputStream in
= null;
753 in
= Instance
.getCache().open(url
, getSupport(url
), true);
754 return IOUtils
.toImage(in
);
755 } catch (IOException e
) {
760 } catch (IOException e
) {
770 * Check if the given resource can be a local image or a remote image, then
771 * refresh the cache with it if it is.
776 * the resource to check
778 * @return the image URL if found, or NULL
781 static URL
getImageUrl(BasicSupport support
, URL source
, String line
) {
787 if (source
!= null) {
788 path
= new File(source
.getFile()).getParent();
790 String basePath
= new File(new File(path
), line
.trim())
792 for (String ext
: getImageExt(true)) {
793 if (new File(basePath
+ ext
).exists()) {
794 url
= new File(basePath
+ ext
).toURI().toURL();
797 } catch (Exception e
) {
798 // Nothing to do here
805 for (String ext
: getImageExt(true)) {
806 if (Instance
.getCache().check(new URL(line
+ ext
))) {
807 url
= new URL(line
+ ext
);
814 for (String ext
: getImageExt(true)) {
816 url
= new URL(line
+ ext
);
817 Instance
.getCache().refresh(url
, support
, true);
819 } catch (IOException e
) {
820 // no image with this ext
825 } catch (MalformedURLException e
) {
830 // refresh the cached file
833 Instance
.getCache().refresh(url
, support
, true);
834 } catch (IOException e
) {
835 // woops, broken image
845 * Open the input file that will be used through the support.
848 * the source {@link URL}
850 * @return the {@link InputStream}
852 * @throws IOException
853 * in case of I/O error
855 protected InputStream
openInput(URL source
) throws IOException
{
856 return Instance
.getCache().open(source
, this, false);
860 * Reset the given {@link InputStream} and return it.
863 * the {@link InputStream} to reset
865 * @return the same {@link InputStream} after reset
867 protected InputStream
reset(InputStream in
) {
870 } catch (IOException e
) {
876 * Reset then return {@link BasicSupport#in}.
878 * @return {@link BasicSupport#in}
880 protected InputStream
getInput() {
885 * Fix the author name if it is prefixed with some "by" {@link String}.
888 * the author with a possible prefix
890 * @return the author without prefixes
892 protected String
fixAuthor(String author
) {
893 if (author
!= null) {
894 for (String suffix
: new String
[] { " ", ":" }) {
895 for (String byString
: Instance
.getConfig()
896 .getString(Config
.BYS
).split(",")) {
898 if (author
.toUpperCase().startsWith(byString
.toUpperCase())) {
899 author
= author
.substring(byString
.length()).trim();
904 // Special case (without suffix):
905 if (author
.startsWith("©")) {
906 author
= author
.substring(1);
914 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
915 * and requotify them (i.e., separate them into QUOTE paragraphs and other
916 * paragraphs (quotes or not)).
919 * the paragraph to requotify (not necessarily a quote)
921 * @return the correctly (or so we hope) quotified paragraphs
923 protected List
<Paragraph
> requotify(Paragraph para
) {
924 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
926 if (para
.getType() == ParagraphType
.QUOTE
927 && para
.getContent().length() > 2) {
928 String line
= para
.getContent();
929 boolean singleQ
= line
.startsWith("" + openQuote
);
930 boolean doubleQ
= line
.startsWith("" + openDoubleQuote
);
932 // Do not try when more than one quote at a time
933 // (some stories are not easily readable if we do)
935 && line
.indexOf(closeQuote
, 1) < line
936 .lastIndexOf(closeQuote
)) {
941 && line
.indexOf(closeDoubleQuote
, 1) < line
942 .lastIndexOf(closeDoubleQuote
)) {
948 if (!singleQ
&& !doubleQ
) {
949 line
= openDoubleQuote
+ line
+ closeDoubleQuote
;
950 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
, para
953 char open
= singleQ ? openQuote
: openDoubleQuote
;
954 char close
= singleQ ? closeQuote
: closeDoubleQuote
;
957 boolean inQuote
= false;
959 for (char car
: line
.toCharArray()) {
962 } else if (car
== close
) {
964 } else if (car
== '.' && !inQuote
) {
972 String rest
= line
.substring(posDot
+ 1).trim();
973 line
= line
.substring(0, posDot
+ 1).trim();
975 for (char car
: line
.toCharArray()) {
980 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
, words
));
981 if (!rest
.isEmpty()) {
982 newParas
.addAll(requotify(processPara(rest
)));
996 * Process a {@link Paragraph} from a raw line of text.
998 * Will also fix quotes and HTML encoding if needed.
1003 * @return the processed {@link Paragraph}
1005 protected Paragraph
processPara(String line
) {
1006 line
= ifUnhtml(line
).trim();
1008 boolean space
= true;
1010 boolean quote
= false;
1011 boolean tentativeCloseQuote
= false;
1016 StringBuilder builder
= new StringBuilder();
1017 for (char car
: line
.toCharArray()) {
1019 if (dashCount
> 0) {
1020 // dash, ndash and mdash: - – —
1021 // currently: always use mdash
1022 builder
.append(dashCount
== 1 ?
'-' : '—');
1027 if (tentativeCloseQuote
) {
1028 tentativeCloseQuote
= false;
1029 if (Character
.isLetterOrDigit(car
)) {
1030 builder
.append("'");
1032 // handle double-single quotes as double quotes
1034 builder
.append(closeDoubleQuote
);
1037 builder
.append(closeQuote
);
1043 case ' ': // note: unbreakable space
1046 case '\n': // just in case
1047 case '\r': // just in case
1048 if (builder
.length() > 0
1049 && builder
.charAt(builder
.length() - 1) != ' ') {
1052 builder
.append(' ');
1056 if (space
|| (brk
&& quote
)) {
1058 // handle double-single quotes as double quotes
1060 builder
.deleteCharAt(builder
.length() - 1);
1061 builder
.append(openDoubleQuote
);
1063 builder
.append(openQuote
);
1065 } else if (prev
== ' ' || prev
== car
) {
1066 // handle double-single quotes as double quotes
1068 builder
.deleteCharAt(builder
.length() - 1);
1069 builder
.append(openDoubleQuote
);
1071 builder
.append(openQuote
);
1074 // it is a quote ("I'm off") or a 'quote' ("This
1075 // 'good' restaurant"...)
1076 tentativeCloseQuote
= true;
1081 if (space
|| (brk
&& quote
)) {
1083 builder
.append(openDoubleQuote
);
1084 } else if (prev
== ' ') {
1085 builder
.append(openDoubleQuote
);
1087 builder
.append(closeDoubleQuote
);
1112 builder
.append(car
);
1121 if (space
|| (brk
&& quote
)) {
1123 builder
.append(openQuote
);
1125 // handle double-single quotes as double quotes
1127 builder
.deleteCharAt(builder
.length() - 1);
1128 builder
.append(openDoubleQuote
);
1130 builder
.append(openQuote
);
1144 // handle double-single quotes as double quotes
1146 builder
.deleteCharAt(builder
.length() - 1);
1147 builder
.append(closeDoubleQuote
);
1149 builder
.append(closeQuote
);
1158 if (space
|| (brk
&& quote
)) {
1160 builder
.append(openDoubleQuote
);
1162 builder
.append(openDoubleQuote
);
1175 builder
.append(closeDoubleQuote
);
1181 builder
.append(car
);
1188 if (tentativeCloseQuote
) {
1189 tentativeCloseQuote
= false;
1190 builder
.append(closeQuote
);
1193 line
= builder
.toString().trim();
1195 ParagraphType type
= ParagraphType
.NORMAL
;
1197 type
= ParagraphType
.BLANK
;
1199 type
= ParagraphType
.BREAK
;
1201 type
= ParagraphType
.QUOTE
;
1204 return new Paragraph(type
, line
, words
);
1208 * Remove the HTML from the input <b>if</b> {@link BasicSupport#isHtml()} is
1214 * @return the no html version if needed
1216 private String
ifUnhtml(String input
) {
1217 if (isHtml() && input
!= null) {
1218 return StringUtils
.unhtml(input
);
1225 * Return a {@link BasicSupport} implementation supporting the given
1226 * resource if possible.
1229 * the story resource
1231 * @return an implementation that supports it, or NULL
1233 public static BasicSupport
getSupport(URL url
) {
1238 // TEXT and INFO_TEXT always support files (not URLs though)
1239 for (SupportType type
: SupportType
.values()) {
1240 if (type
!= SupportType
.TEXT
&& type
!= SupportType
.INFO_TEXT
) {
1241 BasicSupport support
= getSupport(type
);
1242 if (support
!= null && support
.supports(url
)) {
1248 for (SupportType type
: new SupportType
[] { SupportType
.INFO_TEXT
,
1249 SupportType
.TEXT
}) {
1250 BasicSupport support
= getSupport(type
);
1251 if (support
!= null && support
.supports(url
)) {
1260 * Return a {@link BasicSupport} implementation supporting the given type.
1265 * @return an implementation that supports it, or NULL
1267 public static BasicSupport
getSupport(SupportType type
) {
1270 return new Epub().setType(type
);
1272 return new InfoText().setType(type
);
1274 return new Fimfiction().setType(type
);
1276 return new Fanfiction().setType(type
);
1278 return new Text().setType(type
);
1280 return new MangaFox().setType(type
);
1282 return new E621().setType(type
);
1284 return new YiffStar().setType(type
);
1286 return new Cbz().setType(type
);
1288 return new Html().setType(type
);
1295 * Return the first line from the given input which correspond to the given
1301 * a string that must be found inside the target line (also
1302 * supports "^" at start to say "only if it starts with" the
1304 * @param relativeLine
1305 * the line to return based upon the target line position (-1 =
1306 * the line before, 0 = the target line...)
1310 static String
getLine(InputStream in
, String needle
, int relativeLine
) {
1311 return getLine(in
, needle
, relativeLine
, true);
1315 * Return a line from the given input which correspond to the given
1321 * a string that must be found inside the target line (also
1322 * supports "^" at start to say "only if it starts with" the
1324 * @param relativeLine
1325 * the line to return based upon the target line position (-1 =
1326 * the line before, 0 = the target line...)
1328 * takes the first result (as opposed to the last one, which will
1329 * also always spend the input)
1333 static String
getLine(InputStream in
, String needle
, int relativeLine
,
1339 } catch (IOException e
) {
1343 List
<String
> lines
= new ArrayList
<String
>();
1344 @SuppressWarnings("resource")
1345 Scanner scan
= new Scanner(in
, "UTF-8");
1347 scan
.useDelimiter("\\n");
1348 while (scan
.hasNext()) {
1349 lines
.add(scan
.next());
1352 if (needle
.startsWith("^")) {
1353 if (lines
.get(lines
.size() - 1).startsWith(
1354 needle
.substring(1))) {
1355 index
= lines
.size() - 1;
1359 if (lines
.get(lines
.size() - 1).contains(needle
)) {
1360 index
= lines
.size() - 1;
1365 if (index
>= 0 && index
+ relativeLine
< lines
.size()) {
1366 rep
= lines
.get(index
+ relativeLine
);