1. /*
  2. * Copyright 2003-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.filters;
  18. import java.io.IOException;
  19. import java.io.Reader;
  20. import java.util.Vector;
  21. import java.util.Enumeration;
  22. import org.apache.tools.ant.BuildException;
  23. import org.apache.tools.ant.Project;
  24. import org.apache.tools.ant.ProjectComponent;
  25. import org.apache.tools.ant.types.RegularExpression;
  26. import org.apache.tools.ant.types.Substitution;
  27. import org.apache.tools.ant.util.FileUtils;
  28. import org.apache.tools.ant.util.Tokenizer;
  29. import org.apache.tools.ant.util.LineTokenizer;
  30. import org.apache.tools.ant.util.regexp.Regexp;
  31. /**
  32. * This splits up input into tokens and passes
  33. * the tokens to a sequence of filters.
  34. *
  35. * @since Ant 1.6
  36. * @see BaseFilterReader
  37. * @see ChainableReader
  38. * @see org.apache.tools.ant.DynamicConfigurator
  39. */
  40. public class TokenFilter extends BaseFilterReader
  41. implements ChainableReader {
  42. /**
  43. * string filters implement this interface
  44. */
  45. public interface Filter {
  46. /**
  47. * filter and/of modify a string
  48. *
  49. * @param string the string to filter
  50. * @return the modified string or null if the
  51. * string did not pass the filter
  52. */
  53. String filter(String string);
  54. }
  55. /** string filters */
  56. private Vector filters = new Vector();
  57. /** the tokenizer to use on the input stream */
  58. private Tokenizer tokenizer = null;
  59. /** the output token termination */
  60. private String delimOutput = null;
  61. /** the current string token from the input stream */
  62. private String line = null;
  63. /** the position in the current string token */
  64. private int linePos = 0;
  65. /**
  66. * Constructor for "dummy" instances.
  67. *
  68. * @see BaseFilterReader#BaseFilterReader()
  69. */
  70. public TokenFilter() {
  71. super();
  72. }
  73. /**
  74. * Creates a new filtered reader.
  75. *
  76. * @param in A Reader object providing the underlying stream.
  77. * Must not be <code>null</code>.
  78. */
  79. public TokenFilter(final Reader in) {
  80. super(in);
  81. }
  82. /**
  83. * Returns the next character in the filtered stream, only including
  84. * lines from the original stream which match all of the specified
  85. * regular expressions.
  86. *
  87. * @return the next character in the resulting stream, or -1
  88. * if the end of the resulting stream has been reached
  89. *
  90. * @exception IOException if the underlying stream throws an IOException
  91. * during reading
  92. */
  93. public int read() throws IOException {
  94. if (tokenizer == null) {
  95. tokenizer = new LineTokenizer();
  96. }
  97. while (line == null || line.length() == 0) {
  98. line = tokenizer.getToken(in);
  99. if (line == null) {
  100. return -1;
  101. }
  102. for (Enumeration e = filters.elements(); e.hasMoreElements();) {
  103. Filter filter = (Filter) e.nextElement();
  104. line = filter.filter(line);
  105. if (line == null) {
  106. break;
  107. }
  108. }
  109. linePos = 0;
  110. if (line != null) {
  111. if (tokenizer.getPostToken().length() != 0) {
  112. if (delimOutput != null) {
  113. line = line + delimOutput;
  114. } else {
  115. line = line + tokenizer.getPostToken();
  116. }
  117. }
  118. }
  119. }
  120. int ch = line.charAt(linePos);
  121. linePos++;
  122. if (linePos == line.length()) {
  123. line = null;
  124. }
  125. return ch;
  126. }
  127. /**
  128. * Creates a new TokenFilter using the passed in
  129. * Reader for instantiation.
  130. *
  131. * @param reader A Reader object providing the underlying stream.
  132. *
  133. * @return a new filter based on this configuration
  134. */
  135. public final Reader chain(final Reader reader) {
  136. TokenFilter newFilter = new TokenFilter(reader);
  137. newFilter.filters = filters;
  138. newFilter.tokenizer = tokenizer;
  139. newFilter.delimOutput = delimOutput;
  140. newFilter.setProject(getProject());
  141. return newFilter;
  142. }
  143. /**
  144. * set the output delimiter.
  145. * @param delimOutput replaces the delim string returned by the
  146. * tokenizer, if present.
  147. */
  148. public void setDelimOutput(String delimOutput) {
  149. this.delimOutput = resolveBackSlash(delimOutput);
  150. }
  151. // -----------------------------------------
  152. // Predefined tokenizers
  153. // -----------------------------------------
  154. /**
  155. * add a line tokenizer - this is the default.
  156. * @param tokenizer the line tokenizer
  157. */
  158. public void addLineTokenizer(LineTokenizer tokenizer) {
  159. add(tokenizer);
  160. }
  161. /**
  162. * add a string tokenizer
  163. * @param tokenizer the string tokenizer
  164. */
  165. public void addStringTokenizer(StringTokenizer tokenizer) {
  166. add(tokenizer);
  167. }
  168. /**
  169. * add a file tokenizer
  170. * @param tokenizer the file tokenizer
  171. */
  172. public void addFileTokenizer(FileTokenizer tokenizer) {
  173. add(tokenizer);
  174. }
  175. /**
  176. * add an arbitrary tokenizer
  177. * @param tokenizer the tokenizer to all, only one allowed
  178. */
  179. public void add(Tokenizer tokenizer) {
  180. if (this.tokenizer != null) {
  181. throw new BuildException("Only one tokenizer allowed");
  182. }
  183. this.tokenizer = tokenizer;
  184. }
  185. // -----------------------------------------
  186. // Predefined filters
  187. // -----------------------------------------
  188. /**
  189. * replace string filter
  190. * @param filter the replace string filter
  191. */
  192. public void addReplaceString(ReplaceString filter) {
  193. filters.addElement(filter);
  194. }
  195. /**
  196. * contains string filter
  197. * @param filter the contains string filter
  198. */
  199. public void addContainsString(ContainsString filter) {
  200. filters.addElement(filter);
  201. }
  202. /**
  203. * replace regex filter
  204. * @param filter the replace regex filter
  205. */
  206. public void addReplaceRegex(ReplaceRegex filter) {
  207. filters.addElement(filter);
  208. }
  209. /**
  210. * contains regex filter
  211. * @param filter the contains regex filter
  212. */
  213. public void addContainsRegex(ContainsRegex filter) {
  214. filters.addElement(filter);
  215. }
  216. /**
  217. * trim filter
  218. * @param filter the trim filter
  219. */
  220. public void addTrim(Trim filter) {
  221. filters.addElement(filter);
  222. }
  223. /**
  224. * ignore blank filter
  225. * @param filter the ignore blank filter
  226. */
  227. public void addIgnoreBlank(IgnoreBlank filter) {
  228. filters.addElement(filter);
  229. }
  230. /**
  231. * delete chars
  232. * @param filter the delete characters filter
  233. */
  234. public void addDeleteCharacters(DeleteCharacters filter) {
  235. filters.addElement(filter);
  236. }
  237. /**
  238. * Add an arbitrary filter
  239. * @param filter the filter to add
  240. */
  241. public void add(Filter filter) {
  242. filters.addElement(filter);
  243. }
  244. // --------------------------------------------
  245. //
  246. // Tokenizer Classes
  247. //
  248. // --------------------------------------------
  249. /**
  250. * class to read the complete input into a string
  251. */
  252. public static class FileTokenizer extends ProjectComponent
  253. implements Tokenizer {
  254. /**
  255. * Get the complete input as a string
  256. * @param in the reader object
  257. * @return the complete input
  258. * @throws IOException if error reading
  259. */
  260. public String getToken(Reader in) throws IOException {
  261. return FileUtils.readFully(in);
  262. }
  263. /**
  264. * Return the intra-token string
  265. * @return an empty string always
  266. */
  267. public String getPostToken() {
  268. return "";
  269. }
  270. }
  271. /**
  272. * class to tokenize the input as areas separated
  273. * by white space, or by a specified list of
  274. * delim characters. Behaves like java.util.StringTokenizer.
  275. * if the stream starts with delim characters, the first
  276. * token will be an empty string (unless the treat tokens
  277. * as delims flag is set).
  278. */
  279. public static class StringTokenizer extends ProjectComponent
  280. implements Tokenizer {
  281. private String intraString = "";
  282. private int pushed = -2;
  283. private char[] delims = null;
  284. private boolean delimsAreTokens = false;
  285. private boolean suppressDelims = false;
  286. private boolean includeDelims = false;
  287. /**
  288. * attribute delims - the delimiter characters
  289. * @param delims a string containing the delimiter characters
  290. */
  291. public void setDelims(String delims) {
  292. this.delims = resolveBackSlash(delims).toCharArray();
  293. }
  294. /**
  295. * attribute delimsaretokens - treat delimiters as
  296. * separate tokens.
  297. * @param delimsAreTokens true if delimiters are to be separate
  298. */
  299. public void setDelimsAreTokens(boolean delimsAreTokens) {
  300. this.delimsAreTokens = delimsAreTokens;
  301. }
  302. /**
  303. * attribute suppressdelims - suppress delimiters.
  304. * default - false
  305. * @param suppressDelims if true do not report delimiters
  306. */
  307. public void setSuppressDelims(boolean suppressDelims) {
  308. this.suppressDelims = suppressDelims;
  309. }
  310. /**
  311. * attribute includedelims - treat delimiters as part
  312. * of the token.
  313. * default - false
  314. * @param includeDelims if true add delimiters to the token
  315. */
  316. public void setIncludeDelims(boolean includeDelims) {
  317. this.includeDelims = includeDelims;
  318. }
  319. /**
  320. * find and return the next token
  321. *
  322. * @param in the input stream
  323. * @return the token
  324. * @exception IOException if an error occurs reading
  325. */
  326. public String getToken(Reader in) throws IOException {
  327. int ch = -1;
  328. if (pushed != -2) {
  329. ch = pushed;
  330. pushed = -2;
  331. } else {
  332. ch = in.read();
  333. }
  334. if (ch == -1) {
  335. return null;
  336. }
  337. boolean inToken = true;
  338. intraString = "";
  339. StringBuffer word = new StringBuffer();
  340. StringBuffer padding = new StringBuffer();
  341. while (ch != -1) {
  342. char c = (char) ch;
  343. boolean isDelim = isDelim(c);
  344. if (inToken) {
  345. if (isDelim) {
  346. if (delimsAreTokens) {
  347. if (word.length() == 0) {
  348. word.append(c);
  349. } else {
  350. pushed = ch;
  351. }
  352. break;
  353. }
  354. padding.append(c);
  355. inToken = false;
  356. } else {
  357. word.append(c);
  358. }
  359. } else {
  360. if (isDelim) {
  361. padding.append(c);
  362. } else {
  363. pushed = ch;
  364. break;
  365. }
  366. }
  367. ch = in.read();
  368. }
  369. intraString = padding.toString();
  370. if (includeDelims) {
  371. word.append(intraString);
  372. }
  373. return word.toString();
  374. }
  375. /**
  376. * @return the intratoken string
  377. */
  378. public String getPostToken() {
  379. if (suppressDelims || includeDelims) {
  380. return "";
  381. }
  382. return intraString;
  383. }
  384. private boolean isDelim(char ch) {
  385. if (delims == null) {
  386. return Character.isWhitespace(ch);
  387. }
  388. for (int i = 0; i < delims.length; ++i) {
  389. if (delims[i] == ch) {
  390. return true;
  391. }
  392. }
  393. return false;
  394. }
  395. }
  396. // --------------------------------------------
  397. //
  398. // Filter classes
  399. //
  400. // --------------------------------------------
  401. /**
  402. * Abstract class that converts derived filter classes into
  403. * ChainableReaderFilter's
  404. */
  405. public abstract static class ChainableReaderFilter extends ProjectComponent
  406. implements ChainableReader, Filter {
  407. private boolean byLine = true;
  408. /**
  409. * set whether to use filetokenizer or line tokenizer
  410. * @param byLine if true use a linetokenizer (default) otherwise
  411. * use a filetokenizer
  412. */
  413. public void setByLine(boolean byLine) {
  414. this.byLine = byLine;
  415. }
  416. /**
  417. * Chain a tokenfilter reader to a reader,
  418. *
  419. * @param reader the input reader object
  420. * @return the chained reader object
  421. */
  422. public Reader chain(Reader reader) {
  423. TokenFilter tokenFilter = new TokenFilter(reader);
  424. if (!byLine) {
  425. tokenFilter.add(new FileTokenizer());
  426. }
  427. tokenFilter.add(this);
  428. return tokenFilter;
  429. }
  430. }
  431. /**
  432. * Simple replace string filter.
  433. */
  434. public static class ReplaceString extends ChainableReaderFilter {
  435. private String from;
  436. private String to;
  437. /**
  438. * the from attribute
  439. *
  440. * @param from the string to replace
  441. */
  442. public void setFrom(String from) {
  443. this.from = from;
  444. }
  445. /**
  446. * the to attribute
  447. *
  448. * @param to the string to replace 'from' with
  449. */
  450. public void setTo(String to) {
  451. this.to = to;
  452. }
  453. /**
  454. * Filter a string 'line' replacing from with to
  455. * (C&P from the Replace task)
  456. * @param line the string to be filtered
  457. * @return the filtered line
  458. */
  459. public String filter(String line) {
  460. if (from == null) {
  461. throw new BuildException("Missing from in stringreplace");
  462. }
  463. StringBuffer ret = new StringBuffer();
  464. int start = 0;
  465. int found = line.indexOf(from);
  466. while (found >= 0) {
  467. // write everything up to the from
  468. if (found > start) {
  469. ret.append(line.substring(start, found));
  470. }
  471. // write the replacement to
  472. if (to != null) {
  473. ret.append(to);
  474. }
  475. // search again
  476. start = found + from.length();
  477. found = line.indexOf(from, start);
  478. }
  479. // write the remaining characters
  480. if (line.length() > start) {
  481. ret.append(line.substring(start, line.length()));
  482. }
  483. return ret.toString();
  484. }
  485. }
  486. /**
  487. * Simple filter to filter lines contains strings
  488. */
  489. public static class ContainsString extends ProjectComponent
  490. implements Filter {
  491. private String contains;
  492. /**
  493. * the contains attribute
  494. * @param contains the string that the token should contain
  495. */
  496. public void setContains(String contains) {
  497. this.contains = contains;
  498. }
  499. /**
  500. * Filter strings that contain the contains attribute
  501. *
  502. * @param string the string to be filtered
  503. * @return null if the string does not contain "contains",
  504. * string otherwise
  505. */
  506. public String filter(String string) {
  507. if (contains == null) {
  508. throw new BuildException("Missing contains in containsstring");
  509. }
  510. if (string.indexOf(contains) > -1) {
  511. return string;
  512. }
  513. return null;
  514. }
  515. }
  516. /**
  517. * filter to replace regex.
  518. */
  519. public static class ReplaceRegex extends ChainableReaderFilter {
  520. private String from;
  521. private String to;
  522. private RegularExpression regularExpression;
  523. private Substitution substitution;
  524. private boolean initialized = false;
  525. private String flags = "";
  526. private int options;
  527. private Regexp regexp;
  528. /**
  529. * the from attribute
  530. * @param from the regex string
  531. */
  532. public void setPattern(String from) {
  533. this.from = from;
  534. }
  535. /**
  536. * the to attribute
  537. * @param to the replacement string
  538. */
  539. public void setReplace(String to) {
  540. this.to = to;
  541. }
  542. /**
  543. * @param flags the regex flags
  544. */
  545. public void setFlags(String flags) {
  546. this.flags = flags;
  547. }
  548. private void initialize() {
  549. if (initialized) {
  550. return;
  551. }
  552. options = convertRegexOptions(flags);
  553. if (from == null) {
  554. throw new BuildException("Missing pattern in replaceregex");
  555. }
  556. regularExpression = new RegularExpression();
  557. regularExpression.setPattern(from);
  558. regexp = regularExpression.getRegexp(getProject());
  559. if (to == null) {
  560. to = "";
  561. }
  562. substitution = new Substitution();
  563. substitution.setExpression(to);
  564. }
  565. /**
  566. * @param line the string to modify
  567. * @return the modified string
  568. */
  569. public String filter(String line) {
  570. initialize();
  571. if (!regexp.matches(line, options)) {
  572. return line;
  573. }
  574. return regexp.substitute(
  575. line, substitution.getExpression(getProject()), options);
  576. }
  577. }
  578. /**
  579. * filter to filter tokens matching regular expressions.
  580. */
  581. public static class ContainsRegex extends ChainableReaderFilter {
  582. private String from;
  583. private String to;
  584. private Project project;
  585. private RegularExpression regularExpression;
  586. private Substitution substitution;
  587. private boolean initialized = false;
  588. private String flags = "";
  589. private int options;
  590. private Regexp regexp;
  591. /**
  592. * @param from the regex pattern
  593. */
  594. public void setPattern(String from) {
  595. this.from = from;
  596. }
  597. /**
  598. * @param to the replacement string
  599. */
  600. public void setReplace(String to) {
  601. this.to = to;
  602. }
  603. /**
  604. * @param flags the regex flags
  605. */
  606. public void setFlags(String flags) {
  607. this.flags = flags;
  608. }
  609. private void initialize() {
  610. if (initialized) {
  611. return;
  612. }
  613. options = convertRegexOptions(flags);
  614. if (from == null) {
  615. throw new BuildException("Missing from in containsregex");
  616. }
  617. regularExpression = new RegularExpression();
  618. regularExpression.setPattern(from);
  619. regexp = regularExpression.getRegexp(project);
  620. if (to == null) {
  621. return;
  622. }
  623. substitution = new Substitution();
  624. substitution.setExpression(to);
  625. }
  626. /**
  627. * apply regex and substitution on a string
  628. * @param string the string to apply filter on
  629. * @return the filtered string
  630. */
  631. public String filter(String string) {
  632. initialize();
  633. if (!regexp.matches(string, options)) {
  634. return null;
  635. }
  636. if (substitution == null) {
  637. return string;
  638. }
  639. return regexp.substitute(
  640. string, substitution.getExpression(getProject()), options);
  641. }
  642. }
  643. /** Filter to trim white space */
  644. public static class Trim extends ChainableReaderFilter {
  645. /**
  646. * @param line the string to be trimmed
  647. * @return the trimmed string
  648. */
  649. public String filter(String line) {
  650. return line.trim();
  651. }
  652. }
  653. /** Filter remove empty tokens */
  654. public static class IgnoreBlank extends ChainableReaderFilter {
  655. /**
  656. * @param line the line to modify
  657. * @return the trimmed line
  658. */
  659. public String filter(String line) {
  660. if (line.trim().length() == 0) {
  661. return null;
  662. }
  663. return line;
  664. }
  665. }
  666. /**
  667. * Filter to delete characters
  668. */
  669. public static class DeleteCharacters extends ProjectComponent
  670. implements Filter, ChainableReader {
  671. // Attributes
  672. /** the list of characters to remove from the input */
  673. private String deleteChars = "";
  674. /**
  675. * Set the list of characters to delete
  676. * @param deleteChars the list of characters
  677. */
  678. public void setChars(String deleteChars) {
  679. this.deleteChars = resolveBackSlash(deleteChars);
  680. }
  681. /**
  682. * remove characters from a string
  683. * @param string the string to remove the characters from
  684. * @return the converted string
  685. */
  686. public String filter(String string) {
  687. StringBuffer output = new StringBuffer(string.length());
  688. for (int i = 0; i < string.length(); ++i) {
  689. char ch = string.charAt(i);
  690. if (!(isDeleteCharacter(ch))) {
  691. output.append(ch);
  692. }
  693. }
  694. return output.toString();
  695. }
  696. /**
  697. * factory method to provide a reader that removes
  698. * the characters from a reader as part of a filter
  699. * chain
  700. * @param reader the reader object
  701. * @return the chained reader object
  702. */
  703. public Reader chain(Reader reader) {
  704. return new BaseFilterReader(reader) {
  705. /**
  706. * @return the next non delete character
  707. */
  708. public int read()
  709. throws IOException {
  710. while (true) {
  711. int c = in.read();
  712. if (c == -1) {
  713. return c;
  714. }
  715. if (!(isDeleteCharacter((char) c))) {
  716. return c;
  717. }
  718. }
  719. }
  720. };
  721. }
  722. /** check if the character c is to be deleted */
  723. private boolean isDeleteCharacter(char c) {
  724. for (int d = 0; d < deleteChars.length(); ++d) {
  725. if (deleteChars.charAt(d) == c) {
  726. return true;
  727. }
  728. }
  729. return false;
  730. }
  731. }
  732. // --------------------------------------------------------
  733. // static utility methods - could be placed somewhere else
  734. // --------------------------------------------------------
  735. /**
  736. * xml does not do "c" like interpretation of strings.
  737. * i.e. \n\r\t etc.
  738. * this method processes \n, \r, \t, \f, \\
  739. * also subs \s -> " \n\r\t\f"
  740. * a trailing '\' will be ignored
  741. *
  742. * @param input raw string with possible embedded '\'s
  743. * @return converted string
  744. */
  745. public static String resolveBackSlash(String input) {
  746. StringBuffer b = new StringBuffer();
  747. boolean backSlashSeen = false;
  748. for (int i = 0; i < input.length(); ++i) {
  749. char c = input.charAt(i);
  750. if (!backSlashSeen) {
  751. if (c == '\\') {
  752. backSlashSeen = true;
  753. } else {
  754. b.append(c);
  755. }
  756. } else {
  757. switch (c) {
  758. case '\\':
  759. b.append((char) '\\');
  760. break;
  761. case 'n':
  762. b.append((char) '\n');
  763. break;
  764. case 'r':
  765. b.append((char) '\r');
  766. break;
  767. case 't':
  768. b.append((char) '\t');
  769. break;
  770. case 'f':
  771. b.append((char) '\f');
  772. break;
  773. case 's':
  774. b.append(" \t\n\r\f");
  775. break;
  776. default:
  777. b.append(c);
  778. }
  779. backSlashSeen = false;
  780. }
  781. }
  782. return b.toString();
  783. }
  784. /**
  785. * convert regex option flag characters to regex options
  786. * <dl>
  787. * <li>g - Regexp.REPLACE_ALL</li>
  788. * <li>i - Regexp.MATCH_CASE_INSENSITIVE</li>
  789. * <li>m - Regexp.MATCH_MULTILINE</li>
  790. * <li>s - Regexp.MATCH_SINGLELINE</li>
  791. * </dl>
  792. * @param flags the string containing the flags
  793. * @return the Regexp option bits
  794. */
  795. public static int convertRegexOptions(String flags) {
  796. if (flags == null) {
  797. return 0;
  798. }
  799. int options = 0;
  800. if (flags.indexOf('g') != -1) {
  801. options |= Regexp.REPLACE_ALL;
  802. }
  803. if (flags.indexOf('i') != -1) {
  804. options |= Regexp.MATCH_CASE_INSENSITIVE;
  805. }
  806. if (flags.indexOf('m') != -1) {
  807. options |= Regexp.MATCH_MULTILINE;
  808. }
  809. if (flags.indexOf('s') != -1) {
  810. options |= Regexp.MATCH_SINGLELINE;
  811. }
  812. return options;
  813. }
  814. }