1. /*
  2. * Copyright 2000-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.FileNotFoundException;
  23. import java.io.FileOutputStream;
  24. import java.io.FileReader;
  25. import java.io.FileWriter;
  26. import java.io.IOException;
  27. import java.io.InputStreamReader;
  28. import java.io.OutputStreamWriter;
  29. import java.io.Reader;
  30. import java.io.Writer;
  31. import java.util.Enumeration;
  32. import java.util.Properties;
  33. import java.util.Vector;
  34. import org.apache.tools.ant.BuildException;
  35. import org.apache.tools.ant.DirectoryScanner;
  36. import org.apache.tools.ant.Project;
  37. import org.apache.tools.ant.util.FileUtils;
  38. import org.apache.tools.ant.util.StringUtils;
  39. /**
  40. * Replaces all occurrences of one or more string tokens with given
  41. * values in the indicated files. Each value can be either a string
  42. * or the value of a property available in a designated property file.
  43. * If you want to replace a text that crosses line boundaries, you
  44. * must use a nested <code><replacetoken></code> element.
  45. *
  46. * @since Ant 1.1
  47. *
  48. * @ant.task category="filesystem"
  49. */
  50. public class Replace extends MatchingTask {
  51. private File src = null;
  52. private NestedString token = null;
  53. private NestedString value = new NestedString();
  54. private File propertyFile = null;
  55. private File replaceFilterFile = null;
  56. private Properties properties = null;
  57. private Vector replacefilters = new Vector();
  58. private File dir = null;
  59. private int fileCount;
  60. private int replaceCount;
  61. private boolean summary = false;
  62. /** The encoding used to read and write files - if null, uses default */
  63. private String encoding = null;
  64. private FileUtils fileUtils = FileUtils.newFileUtils();
  65. /**
  66. * an inline string to use as the replacement text
  67. */
  68. public class NestedString {
  69. private StringBuffer buf = new StringBuffer();
  70. /**
  71. * the text of the element
  72. *
  73. * @param val the string to add
  74. */
  75. public void addText(String val) {
  76. buf.append(val);
  77. }
  78. /**
  79. * @return the text
  80. */
  81. public String getText() {
  82. return buf.substring(0);
  83. }
  84. }
  85. /**
  86. * A filter to apply.
  87. */
  88. public class Replacefilter {
  89. private String token;
  90. private String value;
  91. private String property;
  92. /**
  93. * validate the filter's configuration
  94. * @throws BuildException if any part is invalid
  95. */
  96. public void validate() throws BuildException {
  97. //Validate mandatory attributes
  98. if (token == null) {
  99. String message = "token is a mandatory attribute "
  100. + "of replacefilter.";
  101. throw new BuildException(message);
  102. }
  103. if ("".equals(token)) {
  104. String message = "The token attribute must not be an empty "
  105. + "string.";
  106. throw new BuildException(message);
  107. }
  108. //value and property are mutually exclusive attributes
  109. if ((value != null) && (property != null)) {
  110. String message = "Either value or property "
  111. + "can be specified, but a replacefilter "
  112. + "element cannot have both.";
  113. throw new BuildException(message);
  114. }
  115. if ((property != null)) {
  116. //the property attribute must have access to a property file
  117. if (propertyFile == null) {
  118. String message = "The replacefilter's property attribute "
  119. + "can only be used with the replacetask's "
  120. + "propertyFile attribute.";
  121. throw new BuildException(message);
  122. }
  123. //Make sure property exists in property file
  124. if (properties == null
  125. || properties.getProperty(property) == null) {
  126. String message = "property \"" + property
  127. + "\" was not found in " + propertyFile.getPath();
  128. throw new BuildException(message);
  129. }
  130. }
  131. }
  132. /**
  133. * Get the replacement value for this filter token.
  134. * @return the replacement value
  135. */
  136. public String getReplaceValue() {
  137. if (property != null) {
  138. return properties.getProperty(property);
  139. } else if (value != null) {
  140. return value;
  141. } else if (Replace.this.value != null) {
  142. return Replace.this.value.getText();
  143. } else {
  144. //Default is empty string
  145. return new String("");
  146. }
  147. }
  148. /**
  149. * Set the token to replace
  150. * @param token token
  151. */
  152. public void setToken(String token) {
  153. this.token = token;
  154. }
  155. /**
  156. * Get the string to search for
  157. * @return current token
  158. */
  159. public String getToken() {
  160. return token;
  161. }
  162. /**
  163. * The replacement string; required if <code>property<code>
  164. * is not set
  165. * @param value value to replace
  166. */
  167. public void setValue(String value) {
  168. this.value = value;
  169. }
  170. /**
  171. * Get replacements string
  172. * @return replacement or null
  173. */
  174. public String getValue() {
  175. return value;
  176. }
  177. /**
  178. * Set the name of the property whose value is to serve as
  179. * the replacement value; required if <code>value</code> is not set.
  180. * @param property propname
  181. */
  182. public void setProperty(String property) {
  183. this.property = property;
  184. }
  185. /**
  186. * Get the name of the property whose value is to serve as
  187. * the replacement value;
  188. * @return property or null
  189. */
  190. public String getProperty() {
  191. return property;
  192. }
  193. }
  194. /**
  195. * Do the execution.
  196. * @throws BuildException if we cant build
  197. */
  198. public void execute() throws BuildException {
  199. Vector savedFilters = (Vector) replacefilters.clone();
  200. Properties savedProperties =
  201. properties == null ? null : (Properties) properties.clone();
  202. try {
  203. if (replaceFilterFile != null) {
  204. Properties props = getProperties(replaceFilterFile);
  205. Enumeration e = props.keys();
  206. while (e.hasMoreElements()) {
  207. String token = e.nextElement().toString();
  208. Replacefilter replaceFilter = createReplacefilter();
  209. replaceFilter.setToken(token);
  210. replaceFilter.setValue(props.getProperty(token));
  211. }
  212. }
  213. validateAttributes();
  214. if (propertyFile != null) {
  215. properties = getProperties(propertyFile);
  216. }
  217. validateReplacefilters();
  218. fileCount = 0;
  219. replaceCount = 0;
  220. if (src != null) {
  221. processFile(src);
  222. }
  223. if (dir != null) {
  224. DirectoryScanner ds = super.getDirectoryScanner(dir);
  225. String[] srcs = ds.getIncludedFiles();
  226. for (int i = 0; i < srcs.length; i++) {
  227. File file = new File(dir, srcs[i]);
  228. processFile(file);
  229. }
  230. }
  231. if (summary) {
  232. log("Replaced " + replaceCount + " occurrences in "
  233. + fileCount + " files.", Project.MSG_INFO);
  234. }
  235. } finally {
  236. replacefilters = savedFilters;
  237. properties = savedProperties;
  238. } // end of finally
  239. }
  240. /**
  241. * Validate attributes provided for this task in .xml build file.
  242. *
  243. * @exception BuildException if any supplied attribute is invalid or any
  244. * mandatory attribute is missing
  245. */
  246. public void validateAttributes() throws BuildException {
  247. if (src == null && dir == null) {
  248. String message = "Either the file or the dir attribute "
  249. + "must be specified";
  250. throw new BuildException(message, getLocation());
  251. }
  252. if (propertyFile != null && !propertyFile.exists()) {
  253. String message = "Property file " + propertyFile.getPath()
  254. + " does not exist.";
  255. throw new BuildException(message, getLocation());
  256. }
  257. if (token == null && replacefilters.size() == 0) {
  258. String message = "Either token or a nested replacefilter "
  259. + "must be specified";
  260. throw new BuildException(message, getLocation());
  261. }
  262. if (token != null && "".equals(token.getText())) {
  263. String message = "The token attribute must not be an empty string.";
  264. throw new BuildException(message, getLocation());
  265. }
  266. }
  267. /**
  268. * Validate nested elements.
  269. *
  270. * @exception BuildException if any supplied attribute is invalid or any
  271. * mandatory attribute is missing
  272. */
  273. public void validateReplacefilters()
  274. throws BuildException {
  275. for (int i = 0; i < replacefilters.size(); i++) {
  276. Replacefilter element =
  277. (Replacefilter) replacefilters.elementAt(i);
  278. element.validate();
  279. }
  280. }
  281. /**
  282. * helper method to load a properties file and throw a build exception
  283. * if it cannot be loaded
  284. * @param propertyFile the file to load the properties from
  285. * @return loaded properties collection
  286. * @throws BuildException if the file could not be found or read
  287. */
  288. public Properties getProperties(File propertyFile) throws BuildException {
  289. Properties properties = new Properties();
  290. try {
  291. properties.load(new FileInputStream(propertyFile));
  292. } catch (FileNotFoundException e) {
  293. String message = "Property file (" + propertyFile.getPath()
  294. + ") not found.";
  295. throw new BuildException(message);
  296. } catch (IOException e) {
  297. String message = "Property file (" + propertyFile.getPath()
  298. + ") cannot be loaded.";
  299. throw new BuildException(message);
  300. }
  301. return properties;
  302. }
  303. /**
  304. * Perform the replacement on the given file.
  305. *
  306. * The replacement is performed on a temporary file which then
  307. * replaces the original file.
  308. *
  309. * @param src the source file
  310. */
  311. private void processFile(File src) throws BuildException {
  312. if (!src.exists()) {
  313. throw new BuildException("Replace: source file " + src.getPath()
  314. + " doesn't exist", getLocation());
  315. }
  316. File temp = fileUtils.createTempFile("rep", ".tmp",
  317. fileUtils.getParentFile(src));
  318. temp.deleteOnExit();
  319. Reader reader = null;
  320. Writer writer = null;
  321. try {
  322. reader = encoding == null ? new FileReader(src)
  323. : new InputStreamReader(new FileInputStream(src), encoding);
  324. writer = encoding == null ? new FileWriter(temp)
  325. : new OutputStreamWriter(new FileOutputStream(temp), encoding);
  326. BufferedReader br = new BufferedReader(reader);
  327. BufferedWriter bw = new BufferedWriter(writer);
  328. String buf = fileUtils.readFully(br);
  329. if (buf == null) {
  330. buf = "";
  331. }
  332. //Preserve original string (buf) so we can compare the result
  333. String newString = new String(buf);
  334. if (token != null) {
  335. // line separators in values and tokens are "\n"
  336. // in order to compare with the file contents, replace them
  337. // as needed
  338. String val = stringReplace(value.getText(), "\r\n",
  339. "\n", false);
  340. val = stringReplace(val, "\n",
  341. StringUtils.LINE_SEP, false);
  342. String tok = stringReplace(token.getText(), "\r\n",
  343. "\n", false);
  344. tok = stringReplace(tok, "\n",
  345. StringUtils.LINE_SEP, false);
  346. // for each found token, replace with value
  347. log("Replacing in " + src.getPath() + ": " + token.getText()
  348. + " --> " + value.getText(), Project.MSG_VERBOSE);
  349. newString = stringReplace(newString, tok, val, true);
  350. }
  351. if (replacefilters.size() > 0) {
  352. newString = processReplacefilters(newString, src.getPath());
  353. }
  354. boolean changes = !newString.equals(buf);
  355. if (changes) {
  356. bw.write(newString, 0, newString.length());
  357. bw.flush();
  358. }
  359. // cleanup
  360. bw.close();
  361. writer = null;
  362. br.close();
  363. reader = null;
  364. // If there were changes, move the new one to the old one;
  365. // otherwise, delete the new one
  366. if (changes) {
  367. ++fileCount;
  368. fileUtils.rename(temp, src);
  369. temp = null;
  370. }
  371. } catch (IOException ioe) {
  372. throw new BuildException("IOException in " + src + " - "
  373. + ioe.getClass().getName() + ":"
  374. + ioe.getMessage(), ioe, getLocation());
  375. } finally {
  376. if (reader != null) {
  377. try {
  378. reader.close();
  379. } catch (IOException e) {
  380. // ignore
  381. }
  382. }
  383. if (writer != null) {
  384. try {
  385. writer.close();
  386. } catch (IOException e) {
  387. // ignore
  388. }
  389. }
  390. if (temp != null) {
  391. temp.delete();
  392. }
  393. }
  394. }
  395. /**
  396. * apply all replace filters to a buffer
  397. * @param buffer string to filter
  398. * @param filename filename for logging purposes
  399. * @return filtered string
  400. */
  401. private String processReplacefilters(String buffer, String filename) {
  402. String newString = new String(buffer);
  403. for (int i = 0; i < replacefilters.size(); i++) {
  404. Replacefilter filter = (Replacefilter) replacefilters.elementAt(i);
  405. //for each found token, replace with value
  406. log("Replacing in " + filename + ": " + filter.getToken()
  407. + " --> " + filter.getReplaceValue(), Project.MSG_VERBOSE);
  408. newString = stringReplace(newString, filter.getToken(),
  409. filter.getReplaceValue(), true);
  410. }
  411. return newString;
  412. }
  413. /**
  414. * Set the source file; required unless <code>dir</code> is set.
  415. * @param file source file
  416. */
  417. public void setFile(File file) {
  418. this.src = file;
  419. }
  420. /**
  421. * Indicates whether a summary of the replace operation should be
  422. * produced, detailing how many token occurrences and files were
  423. * processed; optional, default=false
  424. *
  425. * @param summary true if you would like a summary logged of the
  426. * replace operation
  427. */
  428. public void setSummary(boolean summary) {
  429. this.summary = summary;
  430. }
  431. /**
  432. * Sets the name of a property file containing filters; optional.
  433. * Each property will be treated as a
  434. * replacefilter where token is the name of the property and value
  435. * is the value of the property.
  436. * @param filename file to load
  437. */
  438. public void setReplaceFilterFile(File filename) {
  439. replaceFilterFile = filename;
  440. }
  441. /**
  442. * The base directory to use when replacing a token in multiple files;
  443. * required if <code>file</code> is not defined.
  444. * @param dir base dir
  445. */
  446. public void setDir(File dir) {
  447. this.dir = dir;
  448. }
  449. /**
  450. * Set the string token to replace;
  451. * required unless a nested
  452. * <code>replacetoken</code> element or the <code>replacefilterfile</code>
  453. * attribute is used.
  454. * @param token token string
  455. */
  456. public void setToken(String token) {
  457. createReplaceToken().addText(token);
  458. }
  459. /**
  460. * Set the string value to use as token replacement;
  461. * optional, default is the empty string ""
  462. * @param value replacement value
  463. */
  464. public void setValue(String value) {
  465. createReplaceValue().addText(value);
  466. }
  467. /**
  468. * Set the file encoding to use on the files read and written by the task;
  469. * optional, defaults to default JVM encoding
  470. *
  471. * @param encoding the encoding to use on the files
  472. */
  473. public void setEncoding(String encoding) {
  474. this.encoding = encoding;
  475. }
  476. /**
  477. * the token to filter as the text of a nested element
  478. * @return nested token to configure
  479. */
  480. public NestedString createReplaceToken() {
  481. if (token == null) {
  482. token = new NestedString();
  483. }
  484. return token;
  485. }
  486. /**
  487. * the string to replace the token as the text of a nested element
  488. * @return replacement value to configure
  489. */
  490. public NestedString createReplaceValue() {
  491. return value;
  492. }
  493. /**
  494. * The name of a property file from which properties specified using
  495. * nested <code><replacefilter></code> elements are drawn;
  496. * Required only if <i>property</i> attribute of
  497. * <code><replacefilter></code> is used.
  498. * @param filename file to load
  499. */
  500. public void setPropertyFile(File filename) {
  501. propertyFile = filename;
  502. }
  503. /**
  504. * Add a nested <replacefilter> element.
  505. * @return a nested ReplaceFilter object to be configured
  506. */
  507. public Replacefilter createReplacefilter() {
  508. Replacefilter filter = new Replacefilter();
  509. replacefilters.addElement(filter);
  510. return filter;
  511. }
  512. /**
  513. * Replace occurrences of str1 in string str with str2
  514. */
  515. private String stringReplace(String str, String str1, String str2,
  516. boolean countReplaces) {
  517. StringBuffer ret = new StringBuffer();
  518. int start = 0;
  519. int found = str.indexOf(str1);
  520. while (found >= 0) {
  521. // write everything up to the found str1
  522. if (found > start) {
  523. ret.append(str.substring(start, found));
  524. }
  525. // write the replacement str2
  526. if (str2 != null) {
  527. ret.append(str2);
  528. }
  529. // search again
  530. start = found + str1.length();
  531. found = str.indexOf(str1, start);
  532. if (countReplaces) {
  533. ++replaceCount;
  534. }
  535. }
  536. // write the remaining characters
  537. if (str.length() > start) {
  538. ret.append(str.substring(start, str.length()));
  539. }
  540. return ret.toString();
  541. }
  542. }