1. /*
  2. * Copyright 2001-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.optional;
  18. import java.io.BufferedReader;
  19. import java.io.BufferedWriter;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FileReader;
  23. import java.io.FileOutputStream;
  24. import java.io.FileWriter;
  25. import java.io.InputStreamReader;
  26. import java.io.IOException;
  27. import java.io.OutputStreamWriter;
  28. import java.io.PrintWriter;
  29. import java.io.Reader;
  30. import java.io.Writer;
  31. import java.util.Vector;
  32. import org.apache.tools.ant.BuildException;
  33. import org.apache.tools.ant.DirectoryScanner;
  34. import org.apache.tools.ant.Project;
  35. import org.apache.tools.ant.Task;
  36. import org.apache.tools.ant.types.FileSet;
  37. import org.apache.tools.ant.types.RegularExpression;
  38. import org.apache.tools.ant.types.Substitution;
  39. import org.apache.tools.ant.util.FileUtils;
  40. import org.apache.tools.ant.util.regexp.Regexp;
  41. /**
  42. * Performs regular expression string replacements in a text
  43. * file. The input file(s) must be able to be properly processed by
  44. * a Reader instance. That is, they must be text only, no binary.
  45. *
  46. * The syntax of the regular expression depends on the implementation that
  47. * you choose to use. The system property <code>ant.regexp.regexpimpl</code>
  48. * will be the classname of the implementation that will be used (the default
  49. * is <code>org.apache.tools.ant.util.regexp.JakartaOroRegexp</code> and
  50. * requires the Jakarta Oro Package).
  51. *
  52. * <pre>
  53. * For jdk <= 1.3, there are two available implementations:
  54. * org.apache.tools.ant.util.regexp.JakartaOroRegexp (the default)
  55. * Requires the jakarta-oro package
  56. *
  57. * org.apache.tools.ant.util.regexp.JakartaRegexpRegexp
  58. * Requires the jakarta-regexp package
  59. *
  60. * For jdk >= 1.4 an additional implementation is available:
  61. * org.apache.tools.ant.util.regexp.Jdk14RegexpRegexp
  62. * Requires the jdk 1.4 built in regular expression package.
  63. *
  64. * Usage:
  65. *
  66. * Call Syntax:
  67. *
  68. * <replaceregexp file="file"
  69. * match="pattern"
  70. * replace="pattern"
  71. * flags="options"?
  72. * byline="true|false"? >
  73. * regexp?
  74. * substitution?
  75. * fileset*
  76. * </replaceregexp>
  77. *
  78. * NOTE: You must have either the file attribute specified, or at least one fileset subelement
  79. * to operation on. You may not have the file attribute specified if you nest fileset elements
  80. * inside this task. Also, you cannot specify both match and a regular expression subelement at
  81. * the same time, nor can you specify the replace attribute and the substitution subelement at
  82. * the same time.
  83. *
  84. * Attributes:
  85. *
  86. * file --> A single file to operation on (mutually exclusive
  87. * with the fileset subelements)
  88. * match --> The Regular expression to match
  89. * replace --> The Expression replacement string
  90. * flags --> The options to give to the replacement
  91. * g = Substitute all occurrences. default is to replace only the first one
  92. * i = Case insensitive match
  93. *
  94. * byline --> Should this file be processed a single line at a time (default is false)
  95. * "true" indicates to perform replacement on a line by line basis
  96. * "false" indicates to perform replacement on the whole file at once.
  97. *
  98. * Example:
  99. *
  100. * The following call could be used to replace an old property name in a ".properties"
  101. * file with a new name. In the replace attribute, you can refer to any part of the
  102. * match expression in parenthesis using backslash followed by a number like '\1'.
  103. *
  104. * <replaceregexp file="test.properties"
  105. * match="MyProperty=(.*)"
  106. * replace="NewProperty=\1"
  107. * byline="true" />
  108. *
  109. * </pre>
  110. *
  111. */
  112. public class ReplaceRegExp extends Task {
  113. private File file;
  114. private String flags;
  115. private boolean byline;
  116. private Vector filesets;// Keep jdk 1.1 compliant so others can use this
  117. private RegularExpression regex;
  118. private Substitution subs;
  119. private FileUtils fileUtils = FileUtils.newFileUtils();
  120. /**
  121. * Encoding to assume for the files
  122. */
  123. private String encoding = null;
  124. /** Default Constructor */
  125. public ReplaceRegExp() {
  126. super();
  127. this.file = null;
  128. this.filesets = new Vector();
  129. this.flags = "";
  130. this.byline = false;
  131. this.regex = null;
  132. this.subs = null;
  133. }
  134. /**
  135. * file for which the regular expression should be replaced;
  136. * required unless a nested fileset is supplied.
  137. * @param file The file for which the reg exp should be replaced.
  138. */
  139. public void setFile(File file) {
  140. this.file = file;
  141. }
  142. /**
  143. * the regular expression pattern to match in the file(s);
  144. * required if no nested <regexp> is used
  145. * @param match the match attribute.
  146. */
  147. public void setMatch(String match) {
  148. if (regex != null) {
  149. throw new BuildException("Only one regular expression is allowed");
  150. }
  151. regex = new RegularExpression();
  152. regex.setPattern(match);
  153. }
  154. /**
  155. * The substitution pattern to place in the file(s) in place
  156. * of the regular expression.
  157. * Required if no nested <substitution> is used
  158. * @param replace the replace attribute
  159. */
  160. public void setReplace(String replace) {
  161. if (subs != null) {
  162. throw new BuildException("Only one substitution expression is "
  163. + "allowed");
  164. }
  165. subs = new Substitution();
  166. subs.setExpression(replace);
  167. }
  168. /**
  169. * The flags to use when matching the regular expression. For more
  170. * information, consult the Perl5 syntax.
  171. * <ul>
  172. * <li>g : Global replacement. Replace all occurrences found
  173. * <li>i : Case Insensitive. Do not consider case in the match
  174. * <li>m : Multiline. Treat the string as multiple lines of input,
  175. * using "^" and "$" as the start or end of any line, respectively,
  176. * rather than start or end of string.
  177. * <li> s : Singleline. Treat the string as a single line of input, using
  178. * "." to match any character, including a newline, which normally,
  179. * it would not match.
  180. *</ul>
  181. * @param flags the flags attribute
  182. */
  183. public void setFlags(String flags) {
  184. this.flags = flags;
  185. }
  186. /**
  187. * Process the file(s) one line at a time, executing the replacement
  188. * on one line at a time. This is useful if you
  189. * want to only replace the first occurrence of a regular expression on
  190. * each line, which is not easy to do when processing the file as a whole.
  191. * Defaults to <i>false</i>.</td>
  192. * @param byline the byline attribute as a string
  193. * @deprecated - use setByLine(boolean)
  194. */
  195. public void setByLine(String byline) {
  196. Boolean res = Boolean.valueOf(byline);
  197. if (res == null) {
  198. res = Boolean.FALSE;
  199. }
  200. this.byline = res.booleanValue();
  201. }
  202. /**
  203. * Process the file(s) one line at a time, executing the replacement
  204. * on one line at a time. This is useful if you
  205. * want to only replace the first occurrence of a regular expression on
  206. * each line, which is not easy to do when processing the file as a whole.
  207. * Defaults to <i>false</i>.</td>
  208. * @param byline the byline attribute
  209. */
  210. public void setByLine(boolean byline) {
  211. this.byline = byline;
  212. }
  213. /**
  214. * Specifies the encoding Ant expects the files to be in -
  215. * defaults to the platforms default encoding.
  216. * @param encoding the encoding attribute
  217. *
  218. * @since Ant 1.6
  219. */
  220. public void setEncoding(String encoding) {
  221. this.encoding = encoding;
  222. }
  223. /**
  224. * list files to apply the replacement to
  225. * @param set the fileset element
  226. */
  227. public void addFileset(FileSet set) {
  228. filesets.addElement(set);
  229. }
  230. /**
  231. * A regular expression.
  232. * You can use this element to refer to a previously
  233. * defined regular expression datatype instance
  234. * @return the regular expression object to be configured as an element
  235. */
  236. public RegularExpression createRegexp() {
  237. if (regex != null) {
  238. throw new BuildException("Only one regular expression is allowed.");
  239. }
  240. regex = new RegularExpression();
  241. return regex;
  242. }
  243. /**
  244. * A substitution pattern. You can use this element to refer to a previously
  245. * defined substitution pattern datatype instance.
  246. * @return the substitution pattern object to be configured as an element
  247. */
  248. public Substitution createSubstitution() {
  249. if (subs != null) {
  250. throw new BuildException("Only one substitution expression is "
  251. + "allowed");
  252. }
  253. subs = new Substitution();
  254. return subs;
  255. }
  256. /**
  257. * Invoke a regular expression (r) on a string (input) using
  258. * substitutions (s) for a matching regex.
  259. *
  260. * @param r a regular expression
  261. * @param s a Substitution
  262. * @param input the string to do the replacement on
  263. * @param options The options for the regular expression
  264. * @return the replacement result
  265. */
  266. protected String doReplace(RegularExpression r,
  267. Substitution s,
  268. String input,
  269. int options) {
  270. String res = input;
  271. Regexp regexp = r.getRegexp(getProject());
  272. if (regexp.matches(input, options)) {
  273. log("Found match; substituting", Project.MSG_DEBUG);
  274. res = regexp.substitute(input, s.getExpression(getProject()),
  275. options);
  276. }
  277. return res;
  278. }
  279. /**
  280. * Perform the replacement on a file
  281. *
  282. * @param f the file to perform the relacement on
  283. * @param options the regular expressions options
  284. * @exception IOException if an error occurs
  285. */
  286. protected void doReplace(File f, int options)
  287. throws IOException {
  288. File temp = fileUtils.createTempFile("replace", ".txt", null);
  289. temp.deleteOnExit();
  290. Reader r = null;
  291. Writer w = null;
  292. try {
  293. if (encoding == null) {
  294. r = new FileReader(f);
  295. w = new FileWriter(temp);
  296. } else {
  297. r = new InputStreamReader(new FileInputStream(f), encoding);
  298. w = new OutputStreamWriter(new FileOutputStream(temp),
  299. encoding);
  300. }
  301. BufferedReader br = new BufferedReader(r);
  302. BufferedWriter bw = new BufferedWriter(w);
  303. PrintWriter pw = new PrintWriter(bw);
  304. boolean changes = false;
  305. log("Replacing pattern '" + regex.getPattern(getProject())
  306. + "' with '" + subs.getExpression(getProject())
  307. + "' in '" + f.getPath() + "'" + (byline ? " by line" : "")
  308. + (flags.length() > 0 ? " with flags: '" + flags + "'" : "")
  309. + ".", Project.MSG_VERBOSE);
  310. if (byline) {
  311. StringBuffer linebuf = new StringBuffer();
  312. String line = null;
  313. String res = null;
  314. int c;
  315. boolean hasCR = false;
  316. do {
  317. c = br.read();
  318. if (c == '\r') {
  319. if (hasCR) {
  320. // second CR -> EOL + possibly empty line
  321. line = linebuf.toString();
  322. res = doReplace(regex, subs, line, options);
  323. if (!res.equals(line)) {
  324. changes = true;
  325. }
  326. pw.print(res);
  327. pw.print('\r');
  328. linebuf = new StringBuffer();
  329. // hasCR is still true (for the second one)
  330. } else {
  331. // first CR in this line
  332. hasCR = true;
  333. }
  334. } else if (c == '\n') {
  335. // LF -> EOL
  336. line = linebuf.toString();
  337. res = doReplace(regex, subs, line, options);
  338. if (!res.equals(line)) {
  339. changes = true;
  340. }
  341. pw.print(res);
  342. if (hasCR) {
  343. pw.print('\r');
  344. hasCR = false;
  345. }
  346. pw.print('\n');
  347. linebuf = new StringBuffer();
  348. } else { // any other char
  349. if ((hasCR) || (c < 0)) {
  350. // Mac-style linebreak or EOF (or both)
  351. line = linebuf.toString();
  352. res = doReplace(regex, subs, line, options);
  353. if (!res.equals(line)) {
  354. changes = true;
  355. }
  356. pw.print(res);
  357. if (hasCR) {
  358. pw.print('\r');
  359. hasCR = false;
  360. }
  361. linebuf = new StringBuffer();
  362. }
  363. if (c >= 0) {
  364. linebuf.append((char) c);
  365. }
  366. }
  367. } while (c >= 0);
  368. pw.flush();
  369. } else {
  370. String buf = fileUtils.readFully(br);
  371. if (buf == null) {
  372. buf = "";
  373. }
  374. String res = doReplace(regex, subs, buf, options);
  375. if (!res.equals(buf)) {
  376. changes = true;
  377. }
  378. pw.print(res);
  379. pw.flush();
  380. }
  381. r.close();
  382. r = null;
  383. w.close();
  384. w = null;
  385. if (changes) {
  386. log("File has changed; saving the updated file", Project.MSG_VERBOSE);
  387. try {
  388. fileUtils.rename(temp, f);
  389. temp = null;
  390. } catch (IOException e) {
  391. throw new BuildException("Couldn't rename temporary file "
  392. + temp, getLocation());
  393. }
  394. } else {
  395. log("No change made", Project.MSG_DEBUG);
  396. }
  397. } finally {
  398. try {
  399. if (r != null) {
  400. r.close();
  401. }
  402. } catch (Exception e) {
  403. // ignore any secondary exceptions
  404. }
  405. try {
  406. if (w != null) {
  407. w.close();
  408. }
  409. } catch (Exception e) {
  410. // ignore any secondary exceptions
  411. }
  412. if (temp != null) {
  413. temp.delete();
  414. }
  415. }
  416. }
  417. /**
  418. * Execute the task
  419. *
  420. * @throws BuildException is there is a problem in the task execution.
  421. */
  422. public void execute() throws BuildException {
  423. if (regex == null) {
  424. throw new BuildException("No expression to match.");
  425. }
  426. if (subs == null) {
  427. throw new BuildException("Nothing to replace expression with.");
  428. }
  429. if (file != null && filesets.size() > 0) {
  430. throw new BuildException("You cannot supply the 'file' attribute "
  431. + "and filesets at the same time.");
  432. }
  433. int options = 0;
  434. if (flags.indexOf('g') != -1) {
  435. options |= Regexp.REPLACE_ALL;
  436. }
  437. if (flags.indexOf('i') != -1) {
  438. options |= Regexp.MATCH_CASE_INSENSITIVE;
  439. }
  440. if (flags.indexOf('m') != -1) {
  441. options |= Regexp.MATCH_MULTILINE;
  442. }
  443. if (flags.indexOf('s') != -1) {
  444. options |= Regexp.MATCH_SINGLELINE;
  445. }
  446. if (file != null && file.exists()) {
  447. try {
  448. doReplace(file, options);
  449. } catch (IOException e) {
  450. log("An error occurred processing file: '"
  451. + file.getAbsolutePath() + "': " + e.toString(),
  452. Project.MSG_ERR);
  453. }
  454. } else if (file != null) {
  455. log("The following file is missing: '"
  456. + file.getAbsolutePath() + "'", Project.MSG_ERR);
  457. }
  458. int sz = filesets.size();
  459. for (int i = 0; i < sz; i++) {
  460. FileSet fs = (FileSet) (filesets.elementAt(i));
  461. DirectoryScanner ds = fs.getDirectoryScanner(getProject());
  462. String[] files = ds.getIncludedFiles();
  463. for (int j = 0; j < files.length; j++) {
  464. File f = new File(fs.getDir(getProject()), files[j]);
  465. if (f.exists()) {
  466. try {
  467. doReplace(f, options);
  468. } catch (Exception e) {
  469. log("An error occurred processing file: '"
  470. + f.getAbsolutePath() + "': " + e.toString(),
  471. Project.MSG_ERR);
  472. }
  473. } else {
  474. log("The following file is missing: '"
  475. + f.getAbsolutePath() + "'", Project.MSG_ERR);
  476. }
  477. }
  478. }
  479. }
  480. }