b528cac819acea887acfc28f861b9c32221d1971
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
.HashMap
;
14 import java
.util
.List
;
16 import java
.util
.Map
.Entry
;
17 import java
.util
.Scanner
;
19 import be
.nikiroo
.fanfix
.Instance
;
20 import be
.nikiroo
.fanfix
.bundles
.Config
;
21 import be
.nikiroo
.fanfix
.bundles
.StringId
;
22 import be
.nikiroo
.fanfix
.data
.Chapter
;
23 import be
.nikiroo
.fanfix
.data
.MetaData
;
24 import be
.nikiroo
.fanfix
.data
.Paragraph
;
25 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
26 import be
.nikiroo
.fanfix
.data
.Story
;
27 import be
.nikiroo
.utils
.IOUtils
;
28 import be
.nikiroo
.utils
.Progress
;
29 import be
.nikiroo
.utils
.StringUtils
;
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).
40 public abstract class BasicSupport
{
42 * The supported input types for which we can get a {@link BasicSupport}
47 public enum SupportType
{
48 /** EPUB files created with this program */
50 /** Pure text file with some rules */
52 /** TEXT but with associated .info file */
54 /** My Little Pony fanfictions */
56 /** Fanfictions from a lot of different universes */
58 /** Website with lots of Mangas */
60 /** Furry website with comics support */
62 /** Furry website with stories */
70 * A description of this support type (more information than the
71 * {@link BasicSupport#getSourceName()}).
73 * @return the description
75 public String
getDesc() {
76 String desc
= Instance
.getTrans().getStringX(StringId
.INPUT_DESC
,
80 desc
= Instance
.getTrans().getString(StringId
.INPUT_DESC
, this);
87 * The name of this support type (a short version).
91 public String
getSourceName() {
92 BasicSupport support
= BasicSupport
.getSupport(this);
93 if (support
!= null) {
94 return support
.getSourceName();
101 public String
toString() {
102 return super.toString().toLowerCase();
106 * Call {@link SupportType#valueOf(String.toUpperCase())}.
109 * the possible type name
111 * @return NULL or the type
113 public static SupportType
valueOfUC(String typeName
) {
114 return SupportType
.valueOf(typeName
== null ?
null : typeName
119 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
120 * NULL for NULL instead of raising exception.
123 * the possible type name
125 * @return NULL or the type
127 public static SupportType
valueOfNullOkUC(String typeName
) {
128 if (typeName
== null) {
132 return SupportType
.valueOfUC(typeName
);
136 * Call {@link SupportType#valueOf(String.toUpperCase())} but return
137 * NULL in case of error instead of raising an exception.
140 * the possible type name
142 * @return NULL or the type
144 public static SupportType
valueOfAllOkUC(String typeName
) {
146 return SupportType
.valueOfUC(typeName
);
147 } catch (Exception e
) {
153 private InputStream in
;
154 private SupportType type
;
155 private URL currentReferer
; // with only one 'r', as in 'HTTP'...
158 private char openQuote
= Instance
.getTrans().getChar(
159 StringId
.OPEN_SINGLE_QUOTE
);
160 private char closeQuote
= Instance
.getTrans().getChar(
161 StringId
.CLOSE_SINGLE_QUOTE
);
162 private char openDoubleQuote
= Instance
.getTrans().getChar(
163 StringId
.OPEN_DOUBLE_QUOTE
);
164 private char closeDoubleQuote
= Instance
.getTrans().getChar(
165 StringId
.CLOSE_DOUBLE_QUOTE
);
168 * The name of this support class.
172 protected abstract String
getSourceName();
175 * Check if the given resource is supported by this {@link BasicSupport}.
178 * the resource to check for
180 * @return TRUE if it is
182 protected abstract boolean supports(URL url
);
185 * Return TRUE if the support will return HTML encoded content values for
186 * the chapters content.
188 * @return TRUE for HTML
190 protected abstract boolean isHtml();
192 protected abstract MetaData
getMeta(URL source
, InputStream in
)
196 * Return the story description.
199 * the source of the story
201 * the input (the main resource)
203 * @return the description
205 * @throws IOException
206 * in case of I/O error
208 protected abstract String
getDesc(URL source
, InputStream in
)
212 * Return the list of chapters (name and resource).
215 * the source of the story
217 * the input (the main resource)
219 * @return the chapters
221 * @throws IOException
222 * in case of I/O error
224 protected abstract List
<Entry
<String
, URL
>> getChapters(URL source
,
225 InputStream in
) throws IOException
;
228 * Return the content of the chapter (possibly HTML encoded, if
229 * {@link BasicSupport#isHtml()} is TRUE).
232 * the source of the story
234 * the input (the main resource)
238 * @return the content
240 * @throws IOException
241 * in case of I/O error
243 protected abstract String
getChapterContent(URL source
, InputStream in
,
244 int number
) throws IOException
;
247 * Log into the support (can be a no-op depending upon the support).
249 * @throws IOException
250 * in case of I/O error
252 public void login() throws IOException
{
257 * Return the list of cookies (values included) that must be used to
258 * correctly fetch the resources.
260 * You are expected to call the super method implementation if you override
263 * @return the cookies
265 * @throws IOException
266 * in case of I/O error
268 public Map
<String
, String
> getCookies() throws IOException
{
269 return new HashMap
<String
, String
>();
273 * Return the canonical form of the main {@link URL}.
276 * the source {@link URL}
278 * @return the canonical form of this {@link URL}
280 * @throws IOException
281 * in case of I/O error
283 public URL
getCanonicalUrl(URL source
) throws IOException
{
288 * Process the given story resource into a partially filled {@link Story}
289 * object containing the name and metadata, except for the description.
294 * @return the {@link Story}
296 * @throws IOException
297 * in case of I/O error
299 public Story
processMeta(URL url
) throws IOException
{
300 return processMeta(url
, true, false);
304 * Process the given story resource into a partially filled {@link Story}
305 * object containing the name and metadata.
311 * close "this" and "in" when done
313 * @return the {@link Story}
315 * @throws IOException
316 * in case of I/O error
318 protected Story
processMeta(URL url
, boolean close
, boolean getDesc
)
322 url
= getCanonicalUrl(url
);
324 setCurrentReferer(url
);
332 preprocess(url
, getInput());
334 Story story
= new Story();
335 MetaData meta
= getMeta(url
, getInput());
338 if (meta
!= null && meta
.getCover() == null) {
339 meta
.setCover(getDefaultCover(meta
.getSubject()));
343 String descChapterName
= Instance
.getTrans().getString(
344 StringId
.DESCRIPTION
);
345 story
.getMeta().setResume(
346 makeChapter(url
, 0, descChapterName
,
347 getDesc(url
, getInput())));
355 } catch (IOException e
) {
364 setCurrentReferer(null);
369 * Process the given story resource into a fully filled {@link Story}
375 * the optional progress reporter
377 * @return the {@link Story}
379 * @throws IOException
380 * in case of I/O error
382 public Story
process(URL url
, Progress pg
) throws IOException
{
386 pg
.setMinMax(0, 100);
389 url
= getCanonicalUrl(url
);
392 Story story
= processMeta(url
, false, true);
399 setCurrentReferer(url
);
401 story
.setChapters(new ArrayList
<Chapter
>());
403 List
<Entry
<String
, URL
>> chapters
= getChapters(url
, getInput());
407 if (chapters
!= null) {
408 Progress pgChaps
= new Progress(0, chapters
.size());
409 pg
.addProgress(pgChaps
, 80);
411 for (Entry
<String
, URL
> chap
: chapters
) {
412 setCurrentReferer(chap
.getValue());
413 InputStream chapIn
= Instance
.getCache().open(
414 chap
.getValue(), this, true);
416 story
.getChapters().add(
417 makeChapter(url
, i
, chap
.getKey(),
418 getChapterContent(url
, chapIn
, i
)));
423 pgChaps
.setProgress(i
++);
434 } catch (IOException e
) {
442 setCurrentReferer(null);
451 public SupportType
getType() {
456 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
457 * the current {@link URL} we work on.
459 * @return the referer
461 public URL
getCurrentReferer() {
462 return currentReferer
;
466 * The current referer {@link URL} (only one 'r', as in 'HTML'...), i.e.,
467 * the current {@link URL} we work on.
469 * @param currentReferer
472 protected void setCurrentReferer(URL currentReferer
) {
473 this.currentReferer
= currentReferer
;
484 protected BasicSupport
setType(SupportType type
) {
490 * Prepare the support if needed before processing.
493 * the source of the story
495 * the input (the main resource)
497 * @throws IOException
500 protected void preprocess(URL source
, InputStream in
) throws IOException
{
504 * Now that we have processed the {@link Story}, close the resources if any.
506 * @throws IOException
509 protected void close() throws IOException
{
513 * Create a {@link Chapter} object from the given information, formatting
514 * the content as it should be.
521 * the chapter content
523 * @return the {@link Chapter}
525 * @throws IOException
526 * in case of I/O error
528 protected Chapter
makeChapter(URL source
, int number
, String name
,
529 String content
) throws IOException
{
530 // Chapter name: process it correctly, then remove the possible
531 // redundant "Chapter x: " in front of it
532 String chapterName
= processPara(name
).getContent().trim();
533 for (String lang
: Instance
.getConfig().getString(Config
.CHAPTER
)
535 String chapterWord
= Instance
.getConfig().getStringX(
536 Config
.CHAPTER
, lang
);
537 if (chapterName
.startsWith(chapterWord
)) {
538 chapterName
= chapterName
.substring(chapterWord
.length())
544 if (chapterName
.startsWith(Integer
.toString(number
))) {
545 chapterName
= chapterName
.substring(
546 Integer
.toString(number
).length()).trim();
549 if (chapterName
.startsWith(":")) {
550 chapterName
= chapterName
.substring(1).trim();
554 Chapter chap
= new Chapter(number
, chapterName
);
556 if (content
!= null) {
557 chap
.setParagraphs(makeParagraphs(source
, content
));
565 * Convert the given content into {@link Paragraph}s.
568 * the source URL of the story
570 * the textual content
572 * @return the {@link Paragraph}s
574 * @throws IOException
575 * in case of I/O error
577 protected List
<Paragraph
> makeParagraphs(URL source
, String content
)
580 // Special <HR> processing:
581 content
= content
.replaceAll("(<hr [^>]*>)|(<hr/>)|(<hr>)",
585 List
<Paragraph
> paras
= new ArrayList
<Paragraph
>();
586 InputStream in
= new ByteArrayInputStream(content
.getBytes("UTF-8"));
588 BufferedReader buff
= new BufferedReader(new InputStreamReader(in
,
591 for (String encodedLine
= buff
.readLine(); encodedLine
!= null; encodedLine
= buff
595 lines
= encodedLine
.split("(<p>|</p>|<br>|<br/>|\\n)");
597 lines
= new String
[] { encodedLine
};
600 for (String aline
: lines
) {
601 String line
= aline
.trim();
604 if (line
.startsWith("[") && line
.endsWith("]")) {
605 image
= getImageUrl(this, source
,
606 line
.substring(1, line
.length() - 1).trim());
610 paras
.add(new Paragraph(image
));
612 paras
.add(processPara(line
));
620 // Check quotes for "bad" format
621 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
622 for (Paragraph para
: paras
) {
623 newParas
.addAll(requotify(para
));
627 // Remove double blanks/brks
628 fixBlanksBreaks(paras
);
634 * Fix the {@link ParagraphType#BLANK}s and {@link ParagraphType#BREAK}s of
635 * those {@link Paragraph}s.
637 * The resulting list will not contain a starting or trailing blank/break
638 * nor 2 blanks or breaks following each other.
641 * the list of {@link Paragraph}s to fix
643 protected void fixBlanksBreaks(List
<Paragraph
> paras
) {
644 boolean space
= false;
646 for (int i
= 0; i
< paras
.size(); i
++) {
647 Paragraph para
= paras
.get(i
);
648 boolean thisSpace
= para
.getType() == ParagraphType
.BLANK
;
649 boolean thisBrk
= para
.getType() == ParagraphType
.BREAK
;
651 if (i
> 0 && space
&& thisBrk
) {
654 } else if ((space
|| brk
) && (thisSpace
|| thisBrk
)) {
663 // Remove blank/brk at start
665 && (paras
.get(0).getType() == ParagraphType
.BLANK
|| paras
.get(
666 0).getType() == ParagraphType
.BREAK
)) {
670 // Remove blank/brk at end
671 int last
= paras
.size() - 1;
673 && (paras
.get(last
).getType() == ParagraphType
.BLANK
|| paras
674 .get(last
).getType() == ParagraphType
.BREAK
)) {
680 * Get the default cover related to this subject (see <tt>.info</tt> files).
685 * @return the cover if any, or NULL
687 static BufferedImage
getDefaultCover(String subject
) {
688 if (subject
!= null && !subject
.isEmpty()
689 && Instance
.getCoverDir() != null) {
691 File fileCover
= new File(Instance
.getCoverDir(), subject
);
692 return getImage(null, fileCover
.toURI().toURL(), subject
);
693 } catch (MalformedURLException e
) {
701 * Return the list of supported image extensions.
703 * @param emptyAllowed
704 * TRUE to allow an empty extension on first place, which can be
705 * used when you may already have an extension in your input but
706 * are not sure about it
708 * @return the extensions
710 static String
[] getImageExt(boolean emptyAllowed
) {
712 return new String
[] { "", ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
714 return new String
[] { ".png", ".jpg", ".jpeg", ".gif", ".bmp" };
719 * Check if the given resource can be a local image or a remote image, then
720 * refresh the cache with it if it is.
725 * the resource to check
727 * @return the image if found, or NULL
730 static BufferedImage
getImage(BasicSupport support
, URL source
, String line
) {
731 URL url
= getImageUrl(support
, source
, line
);
733 InputStream in
= null;
735 in
= Instance
.getCache().open(url
, getSupport(url
), true);
736 return IOUtils
.toImage(in
);
737 } catch (IOException e
) {
742 } catch (IOException e
) {
752 * Check if the given resource can be a local image or a remote image, then
753 * refresh the cache with it if it is.
758 * the resource to check
760 * @return the image URL if found, or NULL
763 static URL
getImageUrl(BasicSupport support
, URL source
, String line
) {
769 if (source
!= null) {
770 path
= new File(source
.getFile()).getParent();
772 String basePath
= new File(new File(path
), line
.trim())
774 for (String ext
: getImageExt(true)) {
775 if (new File(basePath
+ ext
).exists()) {
776 url
= new File(basePath
+ ext
).toURI().toURL();
779 } catch (Exception e
) {
780 // Nothing to do here
787 for (String ext
: getImageExt(true)) {
788 if (Instance
.getCache().check(new URL(line
+ ext
))) {
789 url
= new URL(line
+ ext
);
796 for (String ext
: getImageExt(true)) {
798 url
= new URL(line
+ ext
);
799 Instance
.getCache().refresh(url
, support
, true);
801 } catch (IOException e
) {
802 // no image with this ext
807 } catch (MalformedURLException e
) {
812 // refresh the cached file
815 Instance
.getCache().refresh(url
, support
, true);
816 } catch (IOException e
) {
817 // woops, broken image
827 * Open the input file that will be used through the support.
830 * the source {@link URL}
832 * @return the {@link InputStream}
834 * @throws IOException
835 * in case of I/O error
837 protected InputStream
openInput(URL source
) throws IOException
{
838 return Instance
.getCache().open(source
, this, false);
842 * Reset the given {@link InputStream} and return it.
845 * the {@link InputStream} to reset
847 * @return the same {@link InputStream} after reset
849 protected InputStream
reset(InputStream in
) {
852 } catch (IOException e
) {
858 * Reset then return {@link BasicSupport#in}.
860 * @return {@link BasicSupport#in}
862 protected InputStream
getInput() {
867 * Fix the author name if it is prefixed with some "by" {@link String}.
870 * the author with a possible prefix
872 * @return the author without prefixes
874 protected String
fixAuthor(String author
) {
875 if (author
!= null) {
876 for (String suffix
: new String
[] { " ", ":" }) {
877 for (String byString
: Instance
.getConfig()
878 .getString(Config
.BYS
).split(",")) {
880 if (author
.toUpperCase().startsWith(byString
.toUpperCase())) {
881 author
= author
.substring(byString
.length()).trim();
886 // Special case (without suffix):
887 if (author
.startsWith("©")) {
888 author
= author
.substring(1);
896 * Check quotes for bad format (i.e., quotes with normal paragraphs inside)
897 * and requotify them (i.e., separate them into QUOTE paragraphs and other
898 * paragraphs (quotes or not)).
901 * the paragraph to requotify (not necessarily a quote)
903 * @return the correctly (or so we hope) quotified paragraphs
905 protected List
<Paragraph
> requotify(Paragraph para
) {
906 List
<Paragraph
> newParas
= new ArrayList
<Paragraph
>();
908 if (para
.getType() == ParagraphType
.QUOTE
909 && para
.getContent().length() > 2) {
910 String line
= para
.getContent();
911 boolean singleQ
= line
.startsWith("" + openQuote
);
912 boolean doubleQ
= line
.startsWith("" + openDoubleQuote
);
914 // Do not try when more than one quote at a time
915 // (some stories are not easily readable if we do)
917 && line
.indexOf(closeQuote
, 1) < line
918 .lastIndexOf(closeQuote
)) {
923 && line
.indexOf(closeDoubleQuote
, 1) < line
924 .lastIndexOf(closeDoubleQuote
)) {
930 if (!singleQ
&& !doubleQ
) {
931 line
= openDoubleQuote
+ line
+ closeDoubleQuote
;
932 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
));
934 char open
= singleQ ? openQuote
: openDoubleQuote
;
935 char close
= singleQ ? closeQuote
: closeDoubleQuote
;
938 boolean inQuote
= false;
940 for (char car
: line
.toCharArray()) {
943 } else if (car
== close
) {
945 } else if (car
== '.' && !inQuote
) {
953 String rest
= line
.substring(posDot
+ 1).trim();
954 line
= line
.substring(0, posDot
+ 1).trim();
955 newParas
.add(new Paragraph(ParagraphType
.QUOTE
, line
));
956 if (!rest
.isEmpty()) {
957 newParas
.addAll(requotify(processPara(rest
)));
971 * Process a {@link Paragraph} from a raw line of text.
973 * Will also fix quotes and HTML encoding if needed.
978 * @return the processed {@link Paragraph}
980 protected Paragraph
processPara(String line
) {
981 line
= ifUnhtml(line
).trim();
983 boolean space
= true;
985 boolean quote
= false;
986 boolean tentativeCloseQuote
= false;
990 StringBuilder builder
= new StringBuilder();
991 for (char car
: line
.toCharArray()) {
994 // dash, ndash and mdash: - – —
995 // currently: always use mdash
996 builder
.append(dashCount
== 1 ?
'-' : '—');
1001 if (tentativeCloseQuote
) {
1002 tentativeCloseQuote
= false;
1003 if (Character
.isLetterOrDigit(car
)) {
1004 builder
.append("'");
1006 // handle double-single quotes as double quotes
1008 builder
.append(closeDoubleQuote
);
1011 builder
.append(closeQuote
);
1017 case ' ': // note: unbreakable space
1020 case '\n': // just in case
1021 case '\r': // just in case
1022 builder
.append(' ');
1026 if (space
|| (brk
&& quote
)) {
1028 // handle double-single quotes as double quotes
1030 builder
.deleteCharAt(builder
.length() - 1);
1031 builder
.append(openDoubleQuote
);
1033 builder
.append(openQuote
);
1035 } else if (prev
== ' ' || prev
== car
) {
1036 // handle double-single quotes as double quotes
1038 builder
.deleteCharAt(builder
.length() - 1);
1039 builder
.append(openDoubleQuote
);
1041 builder
.append(openQuote
);
1044 // it is a quote ("I'm off") or a 'quote' ("This
1045 // 'good' restaurant"...)
1046 tentativeCloseQuote
= true;
1051 if (space
|| (brk
&& quote
)) {
1053 builder
.append(openDoubleQuote
);
1054 } else if (prev
== ' ') {
1055 builder
.append(openDoubleQuote
);
1057 builder
.append(closeDoubleQuote
);
1082 builder
.append(car
);
1091 if (space
|| (brk
&& quote
)) {
1093 builder
.append(openQuote
);
1095 // handle double-single quotes as double quotes
1097 builder
.deleteCharAt(builder
.length() - 1);
1098 builder
.append(openDoubleQuote
);
1100 builder
.append(openQuote
);
1114 // handle double-single quotes as double quotes
1116 builder
.deleteCharAt(builder
.length() - 1);
1117 builder
.append(closeDoubleQuote
);
1119 builder
.append(closeQuote
);
1128 if (space
|| (brk
&& quote
)) {
1130 builder
.append(openDoubleQuote
);
1132 builder
.append(openDoubleQuote
);
1145 builder
.append(closeDoubleQuote
);
1151 builder
.append(car
);
1158 if (tentativeCloseQuote
) {
1159 tentativeCloseQuote
= false;
1160 builder
.append(closeQuote
);
1163 line
= builder
.toString().trim();
1165 ParagraphType type
= ParagraphType
.NORMAL
;
1167 type
= ParagraphType
.BLANK
;
1169 type
= ParagraphType
.BREAK
;
1171 type
= ParagraphType
.QUOTE
;
1174 return new Paragraph(type
, line
);
1178 * Remove the HTML from the input <b>if</b> {@link BasicSupport#isHtml()} is
1184 * @return the no html version if needed
1186 private String
ifUnhtml(String input
) {
1187 if (isHtml() && input
!= null) {
1188 return StringUtils
.unhtml(input
);
1195 * Return a {@link BasicSupport} implementation supporting the given
1196 * resource if possible.
1199 * the story resource
1201 * @return an implementation that supports it, or NULL
1203 public static BasicSupport
getSupport(URL url
) {
1208 // TEXT and INFO_TEXT always support files (not URLs though)
1209 for (SupportType type
: SupportType
.values()) {
1210 if (type
!= SupportType
.TEXT
&& type
!= SupportType
.INFO_TEXT
) {
1211 BasicSupport support
= getSupport(type
);
1212 if (support
!= null && support
.supports(url
)) {
1218 for (SupportType type
: new SupportType
[] { SupportType
.INFO_TEXT
,
1219 SupportType
.TEXT
}) {
1220 BasicSupport support
= getSupport(type
);
1221 if (support
!= null && support
.supports(url
)) {
1230 * Return a {@link BasicSupport} implementation supporting the given type.
1235 * @return an implementation that supports it, or NULL
1237 public static BasicSupport
getSupport(SupportType type
) {
1240 return new Epub().setType(type
);
1242 return new InfoText().setType(type
);
1244 return new Fimfiction().setType(type
);
1246 return new Fanfiction().setType(type
);
1248 return new Text().setType(type
);
1250 return new MangaFox().setType(type
);
1252 return new E621().setType(type
);
1254 return new YiffStar().setType(type
);
1256 return new Cbz().setType(type
);
1258 return new Html().setType(type
);
1265 * Return the first line from the given input which correspond to the given
1271 * a string that must be found inside the target line (also
1272 * supports "^" at start to say "only if it starts with" the
1274 * @param relativeLine
1275 * the line to return based upon the target line position (-1 =
1276 * the line before, 0 = the target line...)
1280 static String
getLine(InputStream in
, String needle
, int relativeLine
) {
1281 return getLine(in
, needle
, relativeLine
, true);
1285 * Return a line from the given input which correspond to the given
1291 * a string that must be found inside the target line (also
1292 * supports "^" at start to say "only if it starts with" the
1294 * @param relativeLine
1295 * the line to return based upon the target line position (-1 =
1296 * the line before, 0 = the target line...)
1298 * takes the first result (as opposed to the last one, which will
1299 * also always spend the input)
1303 static String
getLine(InputStream in
, String needle
, int relativeLine
,
1309 } catch (IOException e
) {
1313 List
<String
> lines
= new ArrayList
<String
>();
1314 @SuppressWarnings("resource")
1315 Scanner scan
= new Scanner(in
, "UTF-8");
1317 scan
.useDelimiter("\\n");
1318 while (scan
.hasNext()) {
1319 lines
.add(scan
.next());
1322 if (needle
.startsWith("^")) {
1323 if (lines
.get(lines
.size() - 1).startsWith(
1324 needle
.substring(1))) {
1325 index
= lines
.size() - 1;
1329 if (lines
.get(lines
.size() - 1).contains(needle
)) {
1330 index
= lines
.size() - 1;
1335 if (index
>= 0 && index
+ relativeLine
< lines
.size()) {
1336 rep
= lines
.get(index
+ relativeLine
);