1. /*
  2. * Copyright 2002-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs;
  18. import java.io.BufferedReader;
  19. import java.io.BufferedWriter;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FileOutputStream;
  23. import java.io.FileReader;
  24. import java.io.IOException;
  25. import java.io.InputStreamReader;
  26. import java.io.OutputStream;
  27. import java.io.OutputStreamWriter;
  28. import java.io.PrintWriter;
  29. import java.io.Reader;
  30. import java.io.StringReader;
  31. import java.io.Writer;
  32. import java.util.Enumeration;
  33. import java.util.Iterator;
  34. import java.util.Vector;
  35. import org.apache.tools.ant.BuildException;
  36. import org.apache.tools.ant.DirectoryScanner;
  37. import org.apache.tools.ant.Project;
  38. import org.apache.tools.ant.ProjectComponent;
  39. import org.apache.tools.ant.Task;
  40. import org.apache.tools.ant.filters.util.ChainReaderHelper;
  41. import org.apache.tools.ant.types.FileList;
  42. import org.apache.tools.ant.types.FileSet;
  43. import org.apache.tools.ant.types.FilterChain;
  44. import org.apache.tools.ant.types.Path;
  45. import org.apache.tools.ant.util.FileUtils;
  46. /**
  47. * This class contains the 'concat' task, used to concatenate a series
  48. * of files into a single stream. The destination of this stream may
  49. * be the system console, or a file. The following is a sample
  50. * invocation:
  51. *
  52. * <pre>
  53. * <concat destfile="${build.dir}/index.xml"
  54. * append="false">
  55. *
  56. * <fileset dir="${xml.root.dir}"
  57. * includes="*.xml" />
  58. *
  59. * </concat>
  60. * </pre>
  61. *
  62. */
  63. public class Concat extends Task {
  64. // The size of buffers to be used
  65. private static final int BUFFER_SIZE = 8192;
  66. // Attributes.
  67. /**
  68. * The destination of the stream. If <code>null</code>, the system
  69. * console is used.
  70. */
  71. private File destinationFile = null;
  72. /**
  73. * Whether or not the stream should be appended if the destination file
  74. * exists.
  75. * Defaults to <code>false</code>.
  76. */
  77. private boolean append = false;
  78. /**
  79. * Stores the input file encoding.
  80. */
  81. private String encoding = null;
  82. /** Stores the output file encoding. */
  83. private String outputEncoding = null;
  84. /** Stores the binary attribute */
  85. private boolean binary = false;
  86. // Child elements.
  87. /**
  88. * This buffer stores the text within the 'concat' element.
  89. */
  90. private StringBuffer textBuffer;
  91. /**
  92. * Stores a collection of file sets and/or file lists, used to
  93. * select multiple files for concatenation.
  94. */
  95. private Vector sources = new Vector();
  96. /** for filtering the concatenated */
  97. private Vector filterChains = null;
  98. /** ignore dates on input files */
  99. private boolean forceOverwrite = true;
  100. /** String to place at the start of the concatented stream */
  101. private TextElement footer;
  102. /** String to place at the end of the concatented stream */
  103. private TextElement header;
  104. /** add missing line.separator to files **/
  105. private boolean fixLastLine = false;
  106. /** endofline for fixlast line */
  107. private String eolString = System.getProperty("line.separator");
  108. /** outputwriter */
  109. private Writer outputWriter = null;
  110. /** internal variable - used to collect the source files from sources */
  111. private Vector sourceFiles = new Vector();
  112. /** 1.1 utilities and copy utilities */
  113. private static FileUtils fileUtils = FileUtils.newFileUtils();
  114. // Attribute setters.
  115. /**
  116. * Sets the destination file, or uses the console if not specified.
  117. * @param destinationFile the destination file
  118. */
  119. public void setDestfile(File destinationFile) {
  120. this.destinationFile = destinationFile;
  121. }
  122. /**
  123. * Sets the behavior when the destination file exists. If set to
  124. * <code>true</code> the stream data will be appended to the
  125. * existing file, otherwise the existing file will be
  126. * overwritten. Defaults to <code>false</code>.
  127. * @param append if true append to the file.
  128. */
  129. public void setAppend(boolean append) {
  130. this.append = append;
  131. }
  132. /**
  133. * Sets the character encoding
  134. * @param encoding the encoding of the input stream and unless
  135. * outputencoding is set, the outputstream.
  136. */
  137. public void setEncoding(String encoding) {
  138. this.encoding = encoding;
  139. if (outputEncoding == null) {
  140. outputEncoding = encoding;
  141. }
  142. }
  143. /**
  144. * Sets the character encoding for outputting
  145. * @param outputEncoding the encoding for the output file
  146. * @since Ant 1.6
  147. */
  148. public void setOutputEncoding(String outputEncoding) {
  149. this.outputEncoding = outputEncoding;
  150. }
  151. /**
  152. * Force overwrite existing destination file
  153. * @param force if true always overwrite, otherwise only overwrite
  154. * if the output file is older any of the input files.
  155. * @since Ant 1.6
  156. */
  157. public void setForce(boolean force) {
  158. this.forceOverwrite = force;
  159. }
  160. // Nested element creators.
  161. /**
  162. * Path of files to concatenate.
  163. * @return the path used for concatenating
  164. * @since Ant 1.6
  165. */
  166. public Path createPath() {
  167. Path path = new Path(getProject());
  168. sources.addElement(path);
  169. return path;
  170. }
  171. /**
  172. * Set of files to concatenate.
  173. * @param set the set of files
  174. */
  175. public void addFileset(FileSet set) {
  176. sources.addElement(set);
  177. }
  178. /**
  179. * List of files to concatenate.
  180. * @param list the list of files
  181. */
  182. public void addFilelist(FileList list) {
  183. sources.addElement(list);
  184. }
  185. /**
  186. * Adds a FilterChain.
  187. * @param filterChain a filterchain to filter the concatenated input
  188. * @since Ant 1.6
  189. */
  190. public void addFilterChain(FilterChain filterChain) {
  191. if (filterChains == null) {
  192. filterChains = new Vector();
  193. }
  194. filterChains.addElement(filterChain);
  195. }
  196. /**
  197. * This method adds text which appears in the 'concat' element.
  198. * @param text the text to be concated.
  199. */
  200. public void addText(String text) {
  201. if (textBuffer == null) {
  202. // Initialize to the size of the first text fragment, with
  203. // the hopes that it's the only one.
  204. textBuffer = new StringBuffer(text.length());
  205. }
  206. // Append the fragment -- we defer property replacement until
  207. // later just in case we get a partial property in a fragment.
  208. textBuffer.append(text);
  209. }
  210. /**
  211. * Add a header to the concatenated output
  212. * @param header the header
  213. * @since Ant 1.6
  214. */
  215. public void addHeader(TextElement header) {
  216. this.header = header;
  217. }
  218. /**
  219. * Add a footer to the concatenated output
  220. * @param footer the footer
  221. * @since Ant 1.6
  222. */
  223. public void addFooter(TextElement footer) {
  224. this.footer = footer;
  225. }
  226. /**
  227. * Append line.separator to files that do not end
  228. * with a line.separator, default false.
  229. * @param fixLastLine if true make sure each input file has
  230. * new line on the concatenated stream
  231. * @since Ant 1.6
  232. */
  233. public void setFixLastLine(boolean fixLastLine) {
  234. this.fixLastLine = fixLastLine;
  235. }
  236. /**
  237. * Specify the end of line to find and to add if
  238. * not present at end of each input file. This attribute
  239. * is used in conjunction with fixlastline.
  240. * @param crlf the type of new line to add -
  241. * cr, mac, lf, unix, crlf, or dos
  242. * @since Ant 1.6
  243. */
  244. public void setEol(FixCRLF.CrLf crlf) {
  245. String s = crlf.getValue();
  246. if (s.equals("cr") || s.equals("mac")) {
  247. eolString = "\r";
  248. } else if (s.equals("lf") || s.equals("unix")) {
  249. eolString = "\n";
  250. } else if (s.equals("crlf") || s.equals("dos")) {
  251. eolString = "\r\n";
  252. }
  253. }
  254. /**
  255. * set the output writer, this is to allow
  256. * concat to be used as a nested element
  257. * @param outputWriter the output writer
  258. * @since Ant 1.6
  259. */
  260. public void setWriter(Writer outputWriter) {
  261. this.outputWriter = outputWriter;
  262. }
  263. /**
  264. * set the binary attribute.
  265. * if true, concat will concatenate the files
  266. * byte for byte. This mode does not allow
  267. * any filtering, or other modifications
  268. * to the input streams.
  269. * The default value is false.
  270. * @since ant 1.6.2
  271. * @param binary if true, enable binary mode
  272. */
  273. public void setBinary(boolean binary) {
  274. this.binary = binary;
  275. }
  276. /**
  277. * This method performs the concatenation.
  278. */
  279. public void execute() {
  280. // treat empty nested text as no text
  281. sanitizeText();
  282. // if binary check if incompatible attributes are used
  283. if (binary) {
  284. if (destinationFile == null) {
  285. throw new BuildException(
  286. "DestFile attribute is required for binary concatenation");
  287. }
  288. if (textBuffer != null) {
  289. throw new BuildException(
  290. "Nested text is incompatible with binary concatenation");
  291. }
  292. if (encoding != null || outputEncoding != null) {
  293. throw new BuildException(
  294. "Seting input or output encoding is incompatible with binary"
  295. + " concatenation");
  296. }
  297. if (filterChains != null) {
  298. throw new BuildException(
  299. "Setting filters is incompatible with binary concatenation");
  300. }
  301. if (fixLastLine) {
  302. throw new BuildException(
  303. "Setting fixlastline is incompatible with binary concatenation");
  304. }
  305. if (header != null || footer != null) {
  306. throw new BuildException(
  307. "Nested header or footer is incompatible with binary concatenation");
  308. }
  309. }
  310. if (destinationFile != null && outputWriter != null) {
  311. throw new BuildException(
  312. "Cannot specify both a destination file and an output writer");
  313. }
  314. // Sanity check our inputs.
  315. if (sources.size() == 0 && textBuffer == null) {
  316. // Nothing to concatenate!
  317. throw new BuildException(
  318. "At least one file must be provided, or some text.");
  319. }
  320. // If using filesets, disallow inline text. This is similar to
  321. // using GNU 'cat' with file arguments -- stdin is simply
  322. // ignored.
  323. if (sources.size() > 0 && textBuffer != null) {
  324. throw new BuildException(
  325. "Cannot include inline text when using filesets.");
  326. }
  327. // Iterate thru the sources - paths, filesets and filelists
  328. for (Enumeration e = sources.elements(); e.hasMoreElements();) {
  329. Object o = e.nextElement();
  330. if (o instanceof Path) {
  331. Path path = (Path) o;
  332. checkAddFiles(null, path.list());
  333. } else if (o instanceof FileSet) {
  334. FileSet fileSet = (FileSet) o;
  335. DirectoryScanner scanner =
  336. fileSet.getDirectoryScanner(getProject());
  337. checkAddFiles(fileSet.getDir(getProject()),
  338. scanner.getIncludedFiles());
  339. } else if (o instanceof FileList) {
  340. FileList fileList = (FileList) o;
  341. checkAddFiles(fileList.getDir(getProject()),
  342. fileList.getFiles(getProject()));
  343. }
  344. }
  345. // check if the files are outofdate
  346. if (destinationFile != null && !forceOverwrite
  347. && (sourceFiles.size() > 0) && destinationFile.exists()) {
  348. boolean outofdate = false;
  349. for (int i = 0; i < sourceFiles.size(); ++i) {
  350. File file = (File) sourceFiles.elementAt(i);
  351. if (file.lastModified() > destinationFile.lastModified()) {
  352. outofdate = true;
  353. break;
  354. }
  355. }
  356. if (!outofdate) {
  357. log(destinationFile + " is up-to-date.", Project.MSG_VERBOSE);
  358. return; // no need to do anything
  359. }
  360. }
  361. // Do nothing if all the sources are not present
  362. // And textBuffer is null
  363. if (textBuffer == null && sourceFiles.size() == 0
  364. && header == null && footer == null) {
  365. log("No existing files and no nested text, doing nothing",
  366. Project.MSG_INFO);
  367. return;
  368. }
  369. if (binary) {
  370. binaryCat();
  371. } else {
  372. cat();
  373. }
  374. }
  375. /**
  376. * Reset state to default.
  377. */
  378. public void reset() {
  379. append = false;
  380. forceOverwrite = true;
  381. destinationFile = null;
  382. encoding = null;
  383. outputEncoding = null;
  384. fixLastLine = false;
  385. sources.removeAllElements();
  386. sourceFiles.removeAllElements();
  387. filterChains = null;
  388. footer = null;
  389. header = null;
  390. }
  391. private void checkAddFiles(File base, String[] filenames) {
  392. for (int i = 0; i < filenames.length; ++i) {
  393. File file = new File(base, filenames[i]);
  394. if (!file.exists()) {
  395. log("File " + file + " does not exist.", Project.MSG_ERR);
  396. continue;
  397. }
  398. if (destinationFile != null
  399. && fileUtils.fileNameEquals(destinationFile, file)) {
  400. throw new BuildException("Input file \""
  401. + file + "\" "
  402. + "is the same as the output file.");
  403. }
  404. sourceFiles.addElement(file);
  405. }
  406. }
  407. /** perform the binary concatenation */
  408. private void binaryCat() {
  409. log("Binary concatenation of " + sourceFiles.size()
  410. + " files to " + destinationFile);
  411. FileOutputStream out = null;
  412. FileInputStream in = null;
  413. byte[] buffer = new byte[8 * 1024];
  414. try {
  415. try {
  416. out = new FileOutputStream(destinationFile);
  417. } catch (Exception t) {
  418. throw new BuildException(
  419. "Unable to open " + destinationFile
  420. + " for writing", t);
  421. }
  422. for (Iterator i = sourceFiles.iterator(); i.hasNext(); ) {
  423. File sourceFile = (File) i.next();
  424. try {
  425. in = new FileInputStream(sourceFile);
  426. } catch (Exception t) {
  427. throw new BuildException(
  428. "Unable to open input file " + sourceFile,
  429. t);
  430. }
  431. int count = 0;
  432. do {
  433. try {
  434. count = in.read(buffer, 0, buffer.length);
  435. } catch (Exception t) {
  436. throw new BuildException(
  437. "Unable to read from " + sourceFile, t);
  438. }
  439. try {
  440. if (count > 0) {
  441. out.write(buffer, 0, count);
  442. }
  443. } catch (Exception t) {
  444. throw new BuildException(
  445. "Unable to write to " + destinationFile, t);
  446. }
  447. } while (count > 0);
  448. try {
  449. in.close();
  450. } catch (Exception t) {
  451. throw new BuildException(
  452. "Unable to close " + sourceFile, t);
  453. }
  454. in = null;
  455. }
  456. } finally {
  457. if (in != null) {
  458. try {
  459. in.close();
  460. } catch (Throwable t) {
  461. // Ignore
  462. }
  463. }
  464. if (out != null) {
  465. try {
  466. out.close();
  467. } catch (Exception ex) {
  468. throw new BuildException(
  469. "Unable to close " + destinationFile, ex);
  470. }
  471. }
  472. }
  473. }
  474. /** perform the concatenation */
  475. private void cat() {
  476. OutputStream os = null;
  477. Reader reader = null;
  478. char[] buffer = new char[BUFFER_SIZE];
  479. try {
  480. PrintWriter writer = null;
  481. if (outputWriter != null) {
  482. writer = new PrintWriter(outputWriter);
  483. } else {
  484. if (destinationFile == null) {
  485. // Log using WARN so it displays in 'quiet' mode.
  486. os = new LogOutputStream(this, Project.MSG_WARN);
  487. } else {
  488. // ensure that the parent dir of dest file exists
  489. File parent = fileUtils.getParentFile(destinationFile);
  490. if (!parent.exists()) {
  491. parent.mkdirs();
  492. }
  493. os = new FileOutputStream(destinationFile.getAbsolutePath(),
  494. append);
  495. }
  496. if (outputEncoding == null) {
  497. writer = new PrintWriter(
  498. new BufferedWriter(
  499. new OutputStreamWriter(os)));
  500. } else {
  501. writer = new PrintWriter(
  502. new BufferedWriter(
  503. new OutputStreamWriter(os, outputEncoding)));
  504. }
  505. }
  506. if (header != null) {
  507. if (header.getFiltering()) {
  508. concatenate(
  509. buffer, writer, new StringReader(header.getValue()));
  510. } else {
  511. writer.print(header.getValue());
  512. }
  513. }
  514. if (textBuffer != null) {
  515. reader = new StringReader(
  516. getProject().replaceProperties(textBuffer.substring(0)));
  517. } else {
  518. reader = new MultiReader();
  519. }
  520. concatenate(buffer, writer, reader);
  521. if (footer != null) {
  522. if (footer.getFiltering()) {
  523. concatenate(
  524. buffer, writer, new StringReader(footer.getValue()));
  525. } else {
  526. writer.print(footer.getValue());
  527. }
  528. }
  529. writer.flush();
  530. if (os != null) {
  531. os.flush();
  532. }
  533. } catch (IOException ioex) {
  534. throw new BuildException("Error while concatenating: "
  535. + ioex.getMessage(), ioex);
  536. } finally {
  537. if (reader != null) {
  538. try {
  539. reader.close();
  540. } catch (IOException ignore) {
  541. // ignore
  542. }
  543. }
  544. if (os != null) {
  545. try {
  546. os.close();
  547. } catch (IOException ignore) {
  548. // ignore
  549. }
  550. }
  551. }
  552. }
  553. /** Concatenate a single reader to the writer using buffer */
  554. private void concatenate(char[] buffer, Writer writer, Reader in)
  555. throws IOException {
  556. if (filterChains != null) {
  557. ChainReaderHelper helper = new ChainReaderHelper();
  558. helper.setBufferSize(BUFFER_SIZE);
  559. helper.setPrimaryReader(in);
  560. helper.setFilterChains(filterChains);
  561. helper.setProject(getProject());
  562. in = new BufferedReader(helper.getAssembledReader());
  563. }
  564. while (true) {
  565. int nRead = in.read(buffer, 0, buffer.length);
  566. if (nRead == -1) {
  567. break;
  568. }
  569. writer.write(buffer, 0, nRead);
  570. }
  571. writer.flush();
  572. }
  573. /**
  574. * Treat empty nested text as no text.
  575. *
  576. * <p>Depending on the XML parser, addText may have been called
  577. * for "ignorable whitespace" as well.</p>
  578. */
  579. private void sanitizeText() {
  580. if (textBuffer != null) {
  581. if (textBuffer.substring(0).trim().length() == 0) {
  582. textBuffer = null;
  583. }
  584. }
  585. }
  586. /**
  587. * sub element points to a file or contains text
  588. */
  589. public static class TextElement extends ProjectComponent {
  590. private String value = "";
  591. private boolean trimLeading = false;
  592. private boolean trim = false;
  593. private boolean filtering = true;
  594. private String encoding = null;
  595. /**
  596. * whether to filter the text in this element
  597. * or not.
  598. *
  599. * @param filtering true if the text should be filtered.
  600. * the default value is true.
  601. */
  602. public void setFiltering(boolean filtering) {
  603. this.filtering = filtering;
  604. }
  605. /** return the filtering attribute */
  606. private boolean getFiltering() {
  607. return filtering;
  608. }
  609. /**
  610. * The encoding of the text element
  611. *
  612. * @param encoding the name of the charset used to encode
  613. */
  614. public void setEncoding(String encoding) {
  615. this.encoding = encoding;
  616. }
  617. /**
  618. * set the text using a file
  619. * @param file the file to use
  620. * @throws BuildException if the file does not exist, or cannot be
  621. * read
  622. */
  623. public void setFile(File file) {
  624. // non-existing files are not allowed
  625. if (!file.exists()) {
  626. throw new BuildException("File " + file + " does not exist.");
  627. }
  628. BufferedReader reader = null;
  629. try {
  630. if (this.encoding == null) {
  631. reader = new BufferedReader(new FileReader(file));
  632. } else {
  633. reader = new BufferedReader(
  634. new InputStreamReader(new FileInputStream(file),
  635. this.encoding));
  636. }
  637. value = fileUtils.readFully(reader);
  638. } catch (IOException ex) {
  639. throw new BuildException(ex);
  640. } finally {
  641. if (reader != null) {
  642. try {
  643. reader.close();
  644. } catch (Throwable t) {
  645. // ignore
  646. }
  647. }
  648. }
  649. }
  650. /**
  651. * set the text using inline
  652. * @param value the text to place inline
  653. */
  654. public void addText(String value) {
  655. this.value += getProject().replaceProperties(value);
  656. }
  657. /**
  658. * s:^\s*:: on each line of input
  659. * @param strip if true do the trim
  660. */
  661. public void setTrimLeading(boolean strip) {
  662. this.trimLeading = strip;
  663. }
  664. /**
  665. * whether to call text.trim()
  666. * @param trim if true trim the text
  667. */
  668. public void setTrim(boolean trim) {
  669. this.trim = trim;
  670. }
  671. /**
  672. * @return the text, after possible trimming
  673. */
  674. public String getValue() {
  675. if (value == null) {
  676. value = "";
  677. }
  678. if (value.trim().length() == 0) {
  679. value = "";
  680. }
  681. if (trimLeading) {
  682. char[] current = value.toCharArray();
  683. StringBuffer b = new StringBuffer(current.length);
  684. boolean startOfLine = true;
  685. int pos = 0;
  686. while (pos < current.length) {
  687. char ch = current[pos++];
  688. if (startOfLine) {
  689. if (ch == ' ' || ch == '\t') {
  690. continue;
  691. }
  692. startOfLine = false;
  693. }
  694. b.append(ch);
  695. if (ch == '\n' || ch == '\r') {
  696. startOfLine = true;
  697. }
  698. }
  699. value = b.toString();
  700. }
  701. if (trim) {
  702. value = value.trim();
  703. }
  704. return value;
  705. }
  706. }
  707. /**
  708. * This class reads from each of the source files in turn.
  709. * The concatentated result can then be filtered as
  710. * a single stream.
  711. */
  712. private class MultiReader extends Reader {
  713. private int pos = 0;
  714. private Reader reader = null;
  715. private int lastPos = 0;
  716. private char[] lastChars = new char[eolString.length()];
  717. private boolean needAddSeparator = false;
  718. private Reader getReader() throws IOException {
  719. if (reader == null) {
  720. log("Concating file " + sourceFiles.elementAt(pos),
  721. Project.MSG_VERBOSE);
  722. if (encoding == null) {
  723. reader = new BufferedReader(
  724. new FileReader((File) sourceFiles.elementAt(pos)));
  725. } else {
  726. // invoke the zoo of io readers
  727. reader = new BufferedReader(
  728. new InputStreamReader(
  729. new FileInputStream(
  730. (File) sourceFiles.elementAt(pos)),
  731. encoding));
  732. }
  733. for (int i = 0; i < lastChars.length; ++i) {
  734. lastChars[i] = 0;
  735. }
  736. }
  737. return reader;
  738. }
  739. /**
  740. * Read a character from the current reader object. Advance
  741. * to the next if the reader is finished.
  742. * @return the character read, -1 for EOF on the last reader.
  743. * @exception IOException - possibly thrown by the read for a reader
  744. * object.
  745. */
  746. public int read() throws IOException {
  747. if (needAddSeparator) {
  748. int ret = eolString.charAt(lastPos++);
  749. if (lastPos >= eolString.length()) {
  750. lastPos = 0;
  751. needAddSeparator = false;
  752. }
  753. return ret;
  754. }
  755. while (pos < sourceFiles.size()) {
  756. int ch = getReader().read();
  757. if (ch == -1) {
  758. reader.close();
  759. reader = null;
  760. if (fixLastLine && isMissingEndOfLine()) {
  761. needAddSeparator = true;
  762. lastPos = 0;
  763. }
  764. } else {
  765. addLastChar((char) ch);
  766. return ch;
  767. }
  768. pos++;
  769. }
  770. return -1;
  771. }
  772. /**
  773. * Read into the buffer <code>cbuf</code>.
  774. * @param cbuf The array to be read into.
  775. * @param off The offset.
  776. * @param len The length to read.
  777. * @exception IOException - possibly thrown by the reads to the
  778. * reader objects.
  779. */
  780. public int read(char[] cbuf, int off, int len)
  781. throws IOException {
  782. int amountRead = 0;
  783. while (pos < sourceFiles.size() || (needAddSeparator)) {
  784. if (needAddSeparator) {
  785. cbuf[off] = eolString.charAt(lastPos++);
  786. if (lastPos >= eolString.length()) {
  787. lastPos = 0;
  788. needAddSeparator = false;
  789. pos++;
  790. }
  791. len--;
  792. off++;
  793. amountRead++;
  794. if (len == 0) {
  795. return amountRead;
  796. }
  797. continue;
  798. }
  799. int nRead = getReader().read(cbuf, off, len);
  800. if (nRead == -1 || nRead == 0) {
  801. reader.close();
  802. reader = null;
  803. if (fixLastLine && isMissingEndOfLine()) {
  804. needAddSeparator = true;
  805. lastPos = 0;
  806. } else {
  807. pos++;
  808. }
  809. } else {
  810. if (fixLastLine) {
  811. for (int i = nRead;
  812. i > (nRead - lastChars.length);
  813. --i) {
  814. if (i <= 0) {
  815. break;
  816. }
  817. addLastChar(cbuf[off + i - 1]);
  818. }
  819. }
  820. len -= nRead;
  821. off += nRead;
  822. amountRead += nRead;
  823. if (len == 0) {
  824. return amountRead;
  825. }
  826. }
  827. }
  828. if (amountRead == 0) {
  829. return -1;
  830. } else {
  831. return amountRead;
  832. }
  833. }
  834. /**
  835. * Close the current reader
  836. */
  837. public void close() throws IOException {
  838. if (reader != null) {
  839. reader.close();
  840. }
  841. }
  842. /**
  843. * if checking for end of line at end of file
  844. * add a character to the lastchars buffer
  845. */
  846. private void addLastChar(char ch) {
  847. for (int i = lastChars.length - 2; i >= 0; --i) {
  848. lastChars[i] = lastChars[i + 1];
  849. }
  850. lastChars[lastChars.length - 1] = ch;
  851. }
  852. /**
  853. * return true if the lastchars buffer does
  854. * not contain the lineseparator
  855. */
  856. private boolean isMissingEndOfLine() {
  857. for (int i = 0; i < lastChars.length; ++i) {
  858. if (lastChars[i] != eolString.charAt(i)) {
  859. return true;
  860. }
  861. }
  862. return false;
  863. }
  864. }
  865. }