001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.tools.ant.taskdefs;
020:
021: import java.io.File;
022: import java.io.Reader;
023: import java.io.Writer;
024: import java.io.FileReader;
025: import java.io.InputStream;
026: import java.io.IOException;
027: import java.io.PrintWriter;
028: import java.io.OutputStream;
029: import java.io.StringReader;
030: import java.io.BufferedReader;
031: import java.io.BufferedWriter;
032: import java.io.FileInputStream;
033: import java.io.FileOutputStream;
034: import java.io.InputStreamReader;
035: import java.io.OutputStreamWriter;
036: import java.util.Arrays;
037: import java.util.Vector;
038: import java.util.Iterator;
039: import org.apache.tools.ant.Task;
040: import org.apache.tools.ant.Project;
041: import org.apache.tools.ant.BuildException;
042: import org.apache.tools.ant.ProjectComponent;
043: import org.apache.tools.ant.filters.util.ChainReaderHelper;
044: import org.apache.tools.ant.types.Path;
045: import org.apache.tools.ant.types.FileSet;
046: import org.apache.tools.ant.types.FileList;
047: import org.apache.tools.ant.types.FilterChain;
048: import org.apache.tools.ant.types.Resource;
049: import org.apache.tools.ant.types.ResourceCollection;
050: import org.apache.tools.ant.types.resources.Restrict;
051: import org.apache.tools.ant.types.resources.Resources;
052: import org.apache.tools.ant.types.resources.FileResource;
053: import org.apache.tools.ant.types.resources.StringResource;
054: import org.apache.tools.ant.types.resources.selectors.Not;
055: import org.apache.tools.ant.types.resources.selectors.Exists;
056: import org.apache.tools.ant.types.resources.selectors.ResourceSelector;
057: import org.apache.tools.ant.util.FileUtils;
058: import org.apache.tools.ant.util.ConcatResourceInputStream;
059:
060: /**
061: * This class contains the 'concat' task, used to concatenate a series
062: * of files into a single stream. The destination of this stream may
063: * be the system console, or a file. The following is a sample
064: * invocation:
065: *
066: * <pre>
067: * <concat destfile="${build.dir}/index.xml"
068: * append="false">
069: *
070: * <fileset dir="${xml.root.dir}"
071: * includes="*.xml" />
072: *
073: * </concat>
074: * </pre>
075: *
076: */
077: public class Concat extends Task {
078:
079: // The size of buffers to be used
080: private static final int BUFFER_SIZE = 8192;
081:
082: private static final FileUtils FILE_UTILS = FileUtils
083: .getFileUtils();
084:
085: private static final ResourceSelector EXISTS = new Exists();
086: private static final ResourceSelector NOT_EXISTS = new Not(EXISTS);
087:
088: // Attributes.
089:
090: /**
091: * The destination of the stream. If <code>null</code>, the system
092: * console is used.
093: */
094: private File destinationFile;
095:
096: /**
097: * Whether or not the stream should be appended if the destination file
098: * exists.
099: * Defaults to <code>false</code>.
100: */
101: private boolean append;
102:
103: /**
104: * Stores the input file encoding.
105: */
106: private String encoding;
107:
108: /** Stores the output file encoding. */
109: private String outputEncoding;
110:
111: /** Stores the binary attribute */
112: private boolean binary;
113:
114: // Child elements.
115:
116: /**
117: * This buffer stores the text within the 'concat' element.
118: */
119: private StringBuffer textBuffer;
120:
121: /**
122: * Stores a collection of file sets and/or file lists, used to
123: * select multiple files for concatenation.
124: */
125: private Resources rc;
126:
127: /** for filtering the concatenated */
128: private Vector filterChains;
129: /** ignore dates on input files */
130: private boolean forceOverwrite = true;
131: /** String to place at the start of the concatented stream */
132: private TextElement footer;
133: /** String to place at the end of the concatented stream */
134: private TextElement header;
135: /** add missing line.separator to files **/
136: private boolean fixLastLine = false;
137: /** endofline for fixlast line */
138: private String eolString;
139: /** outputwriter */
140: private Writer outputWriter = null;
141:
142: /**
143: * Construct a new Concat task.
144: */
145: public Concat() {
146: reset();
147: }
148:
149: /**
150: * Reset state to default.
151: */
152: public void reset() {
153: append = false;
154: forceOverwrite = true;
155: destinationFile = null;
156: encoding = null;
157: outputEncoding = null;
158: fixLastLine = false;
159: filterChains = null;
160: footer = null;
161: header = null;
162: binary = false;
163: outputWriter = null;
164: textBuffer = null;
165: eolString = System.getProperty("line.separator");
166: rc = null;
167: }
168:
169: // Attribute setters.
170:
171: /**
172: * Sets the destination file, or uses the console if not specified.
173: * @param destinationFile the destination file
174: */
175: public void setDestfile(File destinationFile) {
176: this .destinationFile = destinationFile;
177: }
178:
179: /**
180: * Sets the behavior when the destination file exists. If set to
181: * <code>true</code> the stream data will be appended to the
182: * existing file, otherwise the existing file will be
183: * overwritten. Defaults to <code>false</code>.
184: * @param append if true append to the file.
185: */
186: public void setAppend(boolean append) {
187: this .append = append;
188: }
189:
190: /**
191: * Sets the character encoding
192: * @param encoding the encoding of the input stream and unless
193: * outputencoding is set, the outputstream.
194: */
195: public void setEncoding(String encoding) {
196: this .encoding = encoding;
197: if (outputEncoding == null) {
198: outputEncoding = encoding;
199: }
200: }
201:
202: /**
203: * Sets the character encoding for outputting
204: * @param outputEncoding the encoding for the output file
205: * @since Ant 1.6
206: */
207: public void setOutputEncoding(String outputEncoding) {
208: this .outputEncoding = outputEncoding;
209: }
210:
211: /**
212: * Force overwrite existing destination file
213: * @param force if true always overwrite, otherwise only overwrite
214: * if the output file is older any of the input files.
215: * @since Ant 1.6
216: */
217: public void setForce(boolean force) {
218: this .forceOverwrite = force;
219: }
220:
221: // Nested element creators.
222:
223: /**
224: * Path of files to concatenate.
225: * @return the path used for concatenating
226: * @since Ant 1.6
227: */
228: public Path createPath() {
229: Path path = new Path(getProject());
230: add(path);
231: return path;
232: }
233:
234: /**
235: * Set of files to concatenate.
236: * @param set the set of files
237: */
238: public void addFileset(FileSet set) {
239: add(set);
240: }
241:
242: /**
243: * List of files to concatenate.
244: * @param list the list of files
245: */
246: public void addFilelist(FileList list) {
247: add(list);
248: }
249:
250: /**
251: * Add an arbitrary ResourceCollection.
252: * @param c the ResourceCollection to add.
253: * @since Ant 1.7
254: */
255: public void add(ResourceCollection c) {
256: rc = rc == null ? new Resources() : rc;
257: rc.add(c);
258: }
259:
260: /**
261: * Adds a FilterChain.
262: * @param filterChain a filterchain to filter the concatenated input
263: * @since Ant 1.6
264: */
265: public void addFilterChain(FilterChain filterChain) {
266: if (filterChains == null) {
267: filterChains = new Vector();
268: }
269: filterChains.addElement(filterChain);
270: }
271:
272: /**
273: * This method adds text which appears in the 'concat' element.
274: * @param text the text to be concated.
275: */
276: public void addText(String text) {
277: if (textBuffer == null) {
278: // Initialize to the size of the first text fragment, with
279: // the hopes that it's the only one.
280: textBuffer = new StringBuffer(text.length());
281: }
282:
283: // Append the fragment -- we defer property replacement until
284: // later just in case we get a partial property in a fragment.
285: textBuffer.append(text);
286: }
287:
288: /**
289: * Add a header to the concatenated output
290: * @param headerToAdd the header
291: * @since Ant 1.6
292: */
293: public void addHeader(TextElement headerToAdd) {
294: this .header = headerToAdd;
295: }
296:
297: /**
298: * Add a footer to the concatenated output
299: * @param footerToAdd the footer
300: * @since Ant 1.6
301: */
302: public void addFooter(TextElement footerToAdd) {
303: this .footer = footerToAdd;
304: }
305:
306: /**
307: * Append line.separator to files that do not end
308: * with a line.separator, default false.
309: * @param fixLastLine if true make sure each input file has
310: * new line on the concatenated stream
311: * @since Ant 1.6
312: */
313: public void setFixLastLine(boolean fixLastLine) {
314: this .fixLastLine = fixLastLine;
315: }
316:
317: /**
318: * Specify the end of line to find and to add if
319: * not present at end of each input file. This attribute
320: * is used in conjunction with fixlastline.
321: * @param crlf the type of new line to add -
322: * cr, mac, lf, unix, crlf, or dos
323: * @since Ant 1.6
324: */
325: public void setEol(FixCRLF.CrLf crlf) {
326: String s = crlf.getValue();
327: if (s.equals("cr") || s.equals("mac")) {
328: eolString = "\r";
329: } else if (s.equals("lf") || s.equals("unix")) {
330: eolString = "\n";
331: } else if (s.equals("crlf") || s.equals("dos")) {
332: eolString = "\r\n";
333: }
334: }
335:
336: /**
337: * Set the output writer. This is to allow
338: * concat to be used as a nested element.
339: * @param outputWriter the output writer.
340: * @since Ant 1.6
341: */
342: public void setWriter(Writer outputWriter) {
343: this .outputWriter = outputWriter;
344: }
345:
346: /**
347: * Set the binary attribute. If true, concat will concatenate the files
348: * byte for byte. This mode does not allow any filtering or other
349: * modifications to the input streams. The default value is false.
350: * @since Ant 1.6.2
351: * @param binary if true, enable binary mode.
352: */
353: public void setBinary(boolean binary) {
354: this .binary = binary;
355: }
356:
357: /**
358: * Validate configuration options.
359: */
360: private ResourceCollection validate() {
361:
362: // treat empty nested text as no text
363: sanitizeText();
364:
365: // if binary check if incompatible attributes are used
366: if (binary) {
367: if (destinationFile == null) {
368: throw new BuildException(
369: "destfile attribute is required for binary concatenation");
370: }
371: if (textBuffer != null) {
372: throw new BuildException(
373: "Nested text is incompatible with binary concatenation");
374: }
375: if (encoding != null || outputEncoding != null) {
376: throw new BuildException(
377: "Seting input or output encoding is incompatible with binary"
378: + " concatenation");
379: }
380: if (filterChains != null) {
381: throw new BuildException(
382: "Setting filters is incompatible with binary concatenation");
383: }
384: if (fixLastLine) {
385: throw new BuildException(
386: "Setting fixlastline is incompatible with binary concatenation");
387: }
388: if (header != null || footer != null) {
389: throw new BuildException(
390: "Nested header or footer is incompatible with binary concatenation");
391: }
392: }
393: if (destinationFile != null && outputWriter != null) {
394: throw new BuildException(
395: "Cannot specify both a destination file and an output writer");
396: }
397: // Sanity check our inputs.
398: if (rc == null && textBuffer == null) {
399: // Nothing to concatenate!
400: throw new BuildException(
401: "At least one resource must be provided, or some text.");
402: }
403: if (rc != null) {
404: // If using resources, disallow inline text. This is similar to
405: // using GNU 'cat' with file arguments -- stdin is simply
406: // ignored.
407: if (textBuffer != null) {
408: throw new BuildException(
409: "Cannot include inline text when using resources.");
410: }
411: Restrict noexistRc = new Restrict();
412: noexistRc.add(NOT_EXISTS);
413: noexistRc.add(rc);
414: for (Iterator i = noexistRc.iterator(); i.hasNext();) {
415: log(i.next() + " does not exist.", Project.MSG_ERR);
416: }
417: if (destinationFile != null) {
418: for (Iterator i = rc.iterator(); i.hasNext();) {
419: Object o = i.next();
420: if (o instanceof FileResource) {
421: File f = ((FileResource) o).getFile();
422: if (FILE_UTILS.fileNameEquals(f,
423: destinationFile)) {
424: throw new BuildException(
425: "Input file \""
426: + f
427: + "\" is the same as the output file.");
428: }
429: }
430: }
431: }
432: Restrict existRc = new Restrict();
433: existRc.add(EXISTS);
434: existRc.add(rc);
435: boolean outofdate = destinationFile == null
436: || forceOverwrite;
437: if (!outofdate) {
438: for (Iterator i = existRc.iterator(); !outofdate
439: && i.hasNext();) {
440: Resource r = (Resource) i.next();
441: outofdate = (r.getLastModified() == 0L || r
442: .getLastModified() > destinationFile
443: .lastModified());
444: }
445: }
446: if (!outofdate) {
447: log(destinationFile + " is up-to-date.",
448: Project.MSG_VERBOSE);
449: return null; // no need to do anything
450: }
451: return existRc;
452: } else {
453: StringResource s = new StringResource();
454: s.setProject(getProject());
455: s.setValue(textBuffer.toString());
456: return s;
457: }
458: }
459:
460: /**
461: * Execute the concat task.
462: */
463: public void execute() {
464: ResourceCollection c = validate();
465: if (c == null) {
466: return;
467: }
468: // Do nothing if no resources (including nested text)
469: if (c.size() < 1 && header == null && footer == null) {
470: log(
471: "No existing resources and no nested text, doing nothing",
472: Project.MSG_INFO);
473: return;
474: }
475: if (binary) {
476: binaryCat(c);
477: } else {
478: cat(c);
479: }
480: }
481:
482: /** perform the binary concatenation */
483: private void binaryCat(ResourceCollection c) {
484: log("Binary concatenation of " + c.size() + " resources to "
485: + destinationFile);
486: FileOutputStream out = null;
487: InputStream in = null;
488: try {
489: try {
490: out = new FileOutputStream(destinationFile);
491: } catch (Exception t) {
492: throw new BuildException("Unable to open "
493: + destinationFile + " for writing", t);
494: }
495: in = new ConcatResourceInputStream(c);
496: ((ConcatResourceInputStream) in).setManagingComponent(this );
497: Thread t = new Thread(new StreamPumper(in, out));
498: t.start();
499: try {
500: t.join();
501: } catch (InterruptedException e) {
502: try {
503: t.join();
504: } catch (InterruptedException ee) {
505: // Empty
506: }
507: }
508: } finally {
509: FileUtils.close(in);
510: if (out != null) {
511: try {
512: out.close();
513: } catch (Exception ex) {
514: throw new BuildException("Unable to close "
515: + destinationFile, ex);
516: }
517: }
518: }
519: }
520:
521: /** perform the concatenation */
522: private void cat(ResourceCollection c) {
523: OutputStream os = null;
524: char[] buffer = new char[BUFFER_SIZE];
525:
526: try {
527: PrintWriter writer = null;
528:
529: if (outputWriter != null) {
530: writer = new PrintWriter(outputWriter);
531: } else {
532: if (destinationFile == null) {
533: // Log using WARN so it displays in 'quiet' mode.
534: os = new LogOutputStream(this , Project.MSG_WARN);
535: } else {
536: // ensure that the parent dir of dest file exists
537: File parent = destinationFile.getParentFile();
538: if (!parent.exists()) {
539: parent.mkdirs();
540: }
541: os = new FileOutputStream(destinationFile
542: .getAbsolutePath(), append);
543: }
544: if (outputEncoding == null) {
545: writer = new PrintWriter(new BufferedWriter(
546: new OutputStreamWriter(os)));
547: } else {
548: writer = new PrintWriter(new BufferedWriter(
549: new OutputStreamWriter(os, outputEncoding)));
550: }
551: }
552: if (header != null) {
553: if (header.getFiltering()) {
554: concatenate(buffer, writer, new StringReader(header
555: .getValue()));
556: } else {
557: writer.print(header.getValue());
558: }
559: }
560: if (c.size() > 0) {
561: concatenate(buffer, writer, new MultiReader(c));
562: }
563: if (footer != null) {
564: if (footer.getFiltering()) {
565: concatenate(buffer, writer, new StringReader(footer
566: .getValue()));
567: } else {
568: writer.print(footer.getValue());
569: }
570: }
571: writer.flush();
572: if (os != null) {
573: os.flush();
574: }
575: } catch (IOException ioex) {
576: throw new BuildException("Error while concatenating: "
577: + ioex.getMessage(), ioex);
578: } finally {
579: FileUtils.close(os);
580: }
581: }
582:
583: /** Concatenate a single reader to the writer using buffer */
584: private void concatenate(char[] buffer, Writer writer, Reader in)
585: throws IOException {
586: if (filterChains != null) {
587: ChainReaderHelper helper = new ChainReaderHelper();
588: helper.setBufferSize(BUFFER_SIZE);
589: helper.setPrimaryReader(in);
590: helper.setFilterChains(filterChains);
591: helper.setProject(getProject());
592: in = new BufferedReader(helper.getAssembledReader());
593: }
594: while (true) {
595: int nRead = in.read(buffer, 0, buffer.length);
596: if (nRead == -1) {
597: break;
598: }
599: writer.write(buffer, 0, nRead);
600: }
601: writer.flush();
602: }
603:
604: /**
605: * Treat empty nested text as no text.
606: *
607: * <p>Depending on the XML parser, addText may have been called
608: * for "ignorable whitespace" as well.</p>
609: */
610: private void sanitizeText() {
611: if (textBuffer != null) {
612: if (textBuffer.substring(0).trim().length() == 0) {
613: textBuffer = null;
614: }
615: }
616: }
617:
618: /**
619: * sub element points to a file or contains text
620: */
621: public static class TextElement extends ProjectComponent {
622: private String value = "";
623: private boolean trimLeading = false;
624: private boolean trim = false;
625: private boolean filtering = true;
626: private String encoding = null;
627:
628: /**
629: * whether to filter the text in this element
630: * or not.
631: *
632: * @param filtering true if the text should be filtered.
633: * the default value is true.
634: */
635: public void setFiltering(boolean filtering) {
636: this .filtering = filtering;
637: }
638:
639: /** return the filtering attribute */
640: private boolean getFiltering() {
641: return filtering;
642: }
643:
644: /**
645: * The encoding of the text element
646: *
647: * @param encoding the name of the charset used to encode
648: */
649: public void setEncoding(String encoding) {
650: this .encoding = encoding;
651: }
652:
653: /**
654: * set the text using a file
655: * @param file the file to use
656: * @throws BuildException if the file does not exist, or cannot be
657: * read
658: */
659: public void setFile(File file) throws BuildException {
660: // non-existing files are not allowed
661: if (!file.exists()) {
662: throw new BuildException("File " + file
663: + " does not exist.");
664: }
665:
666: BufferedReader reader = null;
667: try {
668: if (this .encoding == null) {
669: reader = new BufferedReader(new FileReader(file));
670: } else {
671: reader = new BufferedReader(new InputStreamReader(
672: new FileInputStream(file), this .encoding));
673: }
674: value = FileUtils.readFully(reader);
675: } catch (IOException ex) {
676: throw new BuildException(ex);
677: } finally {
678: FileUtils.close(reader);
679: }
680: }
681:
682: /**
683: * set the text using inline
684: * @param value the text to place inline
685: */
686: public void addText(String value) {
687: this .value += getProject().replaceProperties(value);
688: }
689:
690: /**
691: * s:^\s*:: on each line of input
692: * @param strip if true do the trim
693: */
694: public void setTrimLeading(boolean strip) {
695: this .trimLeading = strip;
696: }
697:
698: /**
699: * whether to call text.trim()
700: * @param trim if true trim the text
701: */
702: public void setTrim(boolean trim) {
703: this .trim = trim;
704: }
705:
706: /**
707: * @return the text, after possible trimming
708: */
709: public String getValue() {
710: if (value == null) {
711: value = "";
712: }
713: if (value.trim().length() == 0) {
714: value = "";
715: }
716: if (trimLeading) {
717: char[] current = value.toCharArray();
718: StringBuffer b = new StringBuffer(current.length);
719: boolean startOfLine = true;
720: int pos = 0;
721: while (pos < current.length) {
722: char ch = current[pos++];
723: if (startOfLine) {
724: if (ch == ' ' || ch == '\t') {
725: continue;
726: }
727: startOfLine = false;
728: }
729: b.append(ch);
730: if (ch == '\n' || ch == '\r') {
731: startOfLine = true;
732: }
733: }
734: value = b.toString();
735: }
736: if (trim) {
737: value = value.trim();
738: }
739: return value;
740: }
741: }
742:
743: /**
744: * This class reads from each of the source files in turn.
745: * The concatentated result can then be filtered as
746: * a single stream.
747: */
748: private class MultiReader extends Reader {
749: private Reader reader = null;
750: private int lastPos = 0;
751: private char[] lastChars = new char[eolString.length()];
752: private boolean needAddSeparator = false;
753: private Iterator i;
754:
755: private MultiReader(ResourceCollection c) {
756: i = c.iterator();
757: }
758:
759: private Reader getReader() throws IOException {
760: if (reader == null && i.hasNext()) {
761: Resource r = (Resource) i.next();
762: log("Concating " + r.toLongString(),
763: Project.MSG_VERBOSE);
764: InputStream is = r.getInputStream();
765: reader = new BufferedReader(
766: encoding == null ? new InputStreamReader(is)
767: : new InputStreamReader(is, encoding));
768: Arrays.fill(lastChars, (char) 0);
769: }
770: return reader;
771: }
772:
773: private void nextReader() throws IOException {
774: close();
775: reader = null;
776: }
777:
778: /**
779: * Read a character from the current reader object. Advance
780: * to the next if the reader is finished.
781: * @return the character read, -1 for EOF on the last reader.
782: * @exception IOException - possibly thrown by the read for a reader
783: * object.
784: */
785: public int read() throws IOException {
786: if (needAddSeparator) {
787: int ret = eolString.charAt(lastPos++);
788: if (lastPos >= eolString.length()) {
789: lastPos = 0;
790: needAddSeparator = false;
791: }
792: return ret;
793: }
794: while (getReader() != null) {
795: int ch = getReader().read();
796: if (ch == -1) {
797: nextReader();
798: if (fixLastLine && isMissingEndOfLine()) {
799: needAddSeparator = true;
800: lastPos = 0;
801: }
802: } else {
803: addLastChar((char) ch);
804: return ch;
805: }
806: }
807: return -1;
808: }
809:
810: /**
811: * Read into the buffer <code>cbuf</code>.
812: * @param cbuf The array to be read into.
813: * @param off The offset.
814: * @param len The length to read.
815: * @exception IOException - possibly thrown by the reads to the
816: * reader objects.
817: */
818: public int read(char[] cbuf, int off, int len)
819: throws IOException {
820:
821: int amountRead = 0;
822: while (getReader() != null || needAddSeparator) {
823: if (needAddSeparator) {
824: cbuf[off] = eolString.charAt(lastPos++);
825: if (lastPos >= eolString.length()) {
826: lastPos = 0;
827: needAddSeparator = false;
828: }
829: len--;
830: off++;
831: amountRead++;
832: if (len == 0) {
833: return amountRead;
834: }
835: continue;
836: }
837: int nRead = getReader().read(cbuf, off, len);
838: if (nRead == -1 || nRead == 0) {
839: nextReader();
840: if (fixLastLine && isMissingEndOfLine()) {
841: needAddSeparator = true;
842: lastPos = 0;
843: }
844: } else {
845: if (fixLastLine) {
846: for (int i = nRead; i > (nRead - lastChars.length); --i) {
847: if (i <= 0) {
848: break;
849: }
850: addLastChar(cbuf[off + i - 1]);
851: }
852: }
853: len -= nRead;
854: off += nRead;
855: amountRead += nRead;
856: if (len == 0) {
857: return amountRead;
858: }
859: }
860: }
861: if (amountRead == 0) {
862: return -1;
863: } else {
864: return amountRead;
865: }
866: }
867:
868: /**
869: * Close the current reader
870: */
871: public void close() throws IOException {
872: if (reader != null) {
873: reader.close();
874: }
875: }
876:
877: /**
878: * if checking for end of line at end of file
879: * add a character to the lastchars buffer
880: */
881: private void addLastChar(char ch) {
882: for (int i = lastChars.length - 2; i >= 0; --i) {
883: lastChars[i] = lastChars[i + 1];
884: }
885: lastChars[lastChars.length - 1] = ch;
886: }
887:
888: /**
889: * return true if the lastchars buffer does
890: * not contain the lineseparator
891: */
892: private boolean isMissingEndOfLine() {
893: for (int i = 0; i < lastChars.length; ++i) {
894: if (lastChars[i] != eolString.charAt(i)) {
895: return true;
896: }
897: }
898: return false;
899: }
900: }
901:
902: }
|