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: package org.apache.tools.ant.taskdefs.optional;
019:
020: import java.io.BufferedReader;
021: import java.io.BufferedWriter;
022: import java.io.File;
023: import java.io.FileInputStream;
024: import java.io.FileReader;
025: import java.io.FileOutputStream;
026: import java.io.FileWriter;
027: import java.io.InputStreamReader;
028: import java.io.IOException;
029: import java.io.OutputStreamWriter;
030: import java.io.PrintWriter;
031: import java.io.Reader;
032: import java.io.Writer;
033: import java.util.Vector;
034: import org.apache.tools.ant.BuildException;
035: import org.apache.tools.ant.DirectoryScanner;
036: import org.apache.tools.ant.Project;
037: import org.apache.tools.ant.Task;
038: import org.apache.tools.ant.types.FileSet;
039: import org.apache.tools.ant.types.RegularExpression;
040: import org.apache.tools.ant.types.Substitution;
041: import org.apache.tools.ant.util.FileUtils;
042: import org.apache.tools.ant.util.regexp.Regexp;
043:
044: /**
045: * Performs regular expression string replacements in a text
046: * file. The input file(s) must be able to be properly processed by
047: * a Reader instance. That is, they must be text only, no binary.
048: *
049: * The syntax of the regular expression depends on the implementation that
050: * you choose to use. The system property <code>ant.regexp.regexpimpl</code>
051: * will be the classname of the implementation that will be used (the default
052: * is <code>org.apache.tools.ant.util.regexp.JakartaOroRegexp</code> and
053: * requires the Jakarta Oro Package).
054: *
055: * <pre>
056: * For jdk <= 1.3, there are two available implementations:
057: * org.apache.tools.ant.util.regexp.JakartaOroRegexp (the default)
058: * Requires the jakarta-oro package
059: *
060: * org.apache.tools.ant.util.regexp.JakartaRegexpRegexp
061: * Requires the jakarta-regexp package
062: *
063: * For jdk >= 1.4 an additional implementation is available:
064: * org.apache.tools.ant.util.regexp.Jdk14RegexpRegexp
065: * Requires the jdk 1.4 built in regular expression package.
066: *
067: * Usage:
068: *
069: * Call Syntax:
070: *
071: * <replaceregexp file="file"
072: * match="pattern"
073: * replace="pattern"
074: * flags="options"?
075: * byline="true|false"? >
076: * regexp?
077: * substitution?
078: * fileset*
079: * </replaceregexp>
080: *
081: * NOTE: You must have either the file attribute specified, or at least one fileset subelement
082: * to operation on. You may not have the file attribute specified if you nest fileset elements
083: * inside this task. Also, you cannot specify both match and a regular expression subelement at
084: * the same time, nor can you specify the replace attribute and the substitution subelement at
085: * the same time.
086: *
087: * Attributes:
088: *
089: * file --> A single file to operation on (mutually exclusive
090: * with the fileset subelements)
091: * match --> The Regular expression to match
092: * replace --> The Expression replacement string
093: * flags --> The options to give to the replacement
094: * g = Substitute all occurrences. default is to replace only the first one
095: * i = Case insensitive match
096: *
097: * byline --> Should this file be processed a single line at a time (default is false)
098: * "true" indicates to perform replacement on a line by line basis
099: * "false" indicates to perform replacement on the whole file at once.
100: *
101: * Example:
102: *
103: * The following call could be used to replace an old property name in a ".properties"
104: * file with a new name. In the replace attribute, you can refer to any part of the
105: * match expression in parenthesis using backslash followed by a number like '\1'.
106: *
107: * <replaceregexp file="test.properties"
108: * match="MyProperty=(.*)"
109: * replace="NewProperty=\1"
110: * byline="true" />
111: *
112: * </pre>
113: *
114: */
115: public class ReplaceRegExp extends Task {
116:
117: private File file;
118: private String flags;
119: private boolean byline;
120: private Vector filesets; // Keep jdk 1.1 compliant so others can use this
121: private RegularExpression regex;
122: private Substitution subs;
123:
124: private static final FileUtils FILE_UTILS = FileUtils
125: .getFileUtils();
126:
127: /**
128: * Encoding to assume for the files
129: */
130: private String encoding = null;
131:
132: /** Default Constructor */
133: public ReplaceRegExp() {
134: super ();
135: this .file = null;
136: this .filesets = new Vector();
137: this .flags = "";
138: this .byline = false;
139:
140: this .regex = null;
141: this .subs = null;
142: }
143:
144: /**
145: * file for which the regular expression should be replaced;
146: * required unless a nested fileset is supplied.
147: * @param file The file for which the reg exp should be replaced.
148: */
149: public void setFile(File file) {
150: this .file = file;
151: }
152:
153: /**
154: * the regular expression pattern to match in the file(s);
155: * required if no nested <regexp> is used
156: * @param match the match attribute.
157: */
158: public void setMatch(String match) {
159: if (regex != null) {
160: throw new BuildException(
161: "Only one regular expression is allowed");
162: }
163:
164: regex = new RegularExpression();
165: regex.setPattern(match);
166: }
167:
168: /**
169: * The substitution pattern to place in the file(s) in place
170: * of the regular expression.
171: * Required if no nested <substitution> is used
172: * @param replace the replace attribute
173: */
174:
175: public void setReplace(String replace) {
176: if (subs != null) {
177: throw new BuildException(
178: "Only one substitution expression is " + "allowed");
179: }
180:
181: subs = new Substitution();
182: subs.setExpression(replace);
183: }
184:
185: /**
186: * The flags to use when matching the regular expression. For more
187: * information, consult the Perl5 syntax.
188: * <ul>
189: * <li>g : Global replacement. Replace all occurrences found
190: * <li>i : Case Insensitive. Do not consider case in the match
191: * <li>m : Multiline. Treat the string as multiple lines of input,
192: * using "^" and "$" as the start or end of any line, respectively,
193: * rather than start or end of string.
194: * <li> s : Singleline. Treat the string as a single line of input, using
195: * "." to match any character, including a newline, which normally,
196: * it would not match.
197: *</ul>
198: * @param flags the flags attribute
199: */
200: public void setFlags(String flags) {
201: this .flags = flags;
202: }
203:
204: /**
205: * Process the file(s) one line at a time, executing the replacement
206: * on one line at a time. This is useful if you
207: * want to only replace the first occurrence of a regular expression on
208: * each line, which is not easy to do when processing the file as a whole.
209: * Defaults to <i>false</i>.</td>
210: * @param byline the byline attribute as a string
211: * @deprecated since 1.6.x.
212: * Use setByLine(boolean).
213: */
214: public void setByLine(String byline) {
215: Boolean res = Boolean.valueOf(byline);
216:
217: if (res == null) {
218: res = Boolean.FALSE;
219: }
220: this .byline = res.booleanValue();
221: }
222:
223: /**
224: * Process the file(s) one line at a time, executing the replacement
225: * on one line at a time. This is useful if you
226: * want to only replace the first occurrence of a regular expression on
227: * each line, which is not easy to do when processing the file as a whole.
228: * Defaults to <i>false</i>.</td>
229: * @param byline the byline attribute
230: */
231: public void setByLine(boolean byline) {
232: this .byline = byline;
233: }
234:
235: /**
236: * Specifies the encoding Ant expects the files to be in -
237: * defaults to the platforms default encoding.
238: * @param encoding the encoding attribute
239: *
240: * @since Ant 1.6
241: */
242: public void setEncoding(String encoding) {
243: this .encoding = encoding;
244: }
245:
246: /**
247: * list files to apply the replacement to
248: * @param set the fileset element
249: */
250: public void addFileset(FileSet set) {
251: filesets.addElement(set);
252: }
253:
254: /**
255: * A regular expression.
256: * You can use this element to refer to a previously
257: * defined regular expression datatype instance
258: * @return the regular expression object to be configured as an element
259: */
260: public RegularExpression createRegexp() {
261: if (regex != null) {
262: throw new BuildException(
263: "Only one regular expression is allowed.");
264: }
265:
266: regex = new RegularExpression();
267: return regex;
268: }
269:
270: /**
271: * A substitution pattern. You can use this element to refer to a previously
272: * defined substitution pattern datatype instance.
273: * @return the substitution pattern object to be configured as an element
274: */
275: public Substitution createSubstitution() {
276: if (subs != null) {
277: throw new BuildException(
278: "Only one substitution expression is " + "allowed");
279: }
280:
281: subs = new Substitution();
282: return subs;
283: }
284:
285: /**
286: * Invoke a regular expression (r) on a string (input) using
287: * substitutions (s) for a matching regex.
288: *
289: * @param r a regular expression
290: * @param s a Substitution
291: * @param input the string to do the replacement on
292: * @param options The options for the regular expression
293: * @return the replacement result
294: */
295: protected String doReplace(RegularExpression r, Substitution s,
296: String input, int options) {
297: String res = input;
298: Regexp regexp = r.getRegexp(getProject());
299:
300: if (regexp.matches(input, options)) {
301: log("Found match; substituting", Project.MSG_DEBUG);
302: res = regexp.substitute(input, s
303: .getExpression(getProject()), options);
304: }
305:
306: return res;
307: }
308:
309: /**
310: * Perform the replacement on a file
311: *
312: * @param f the file to perform the relacement on
313: * @param options the regular expressions options
314: * @exception IOException if an error occurs
315: */
316: protected void doReplace(File f, int options) throws IOException {
317: File temp = FILE_UTILS.createTempFile("replace", ".txt", null);
318: temp.deleteOnExit();
319:
320: Reader r = null;
321: Writer w = null;
322:
323: try {
324: if (encoding == null) {
325: r = new FileReader(f);
326: w = new FileWriter(temp);
327: } else {
328: r = new InputStreamReader(new FileInputStream(f),
329: encoding);
330: w = new OutputStreamWriter(new FileOutputStream(temp),
331: encoding);
332: }
333:
334: BufferedReader br = new BufferedReader(r);
335: BufferedWriter bw = new BufferedWriter(w);
336: PrintWriter pw = new PrintWriter(bw);
337:
338: boolean changes = false;
339:
340: log("Replacing pattern '"
341: + regex.getPattern(getProject())
342: + "' with '"
343: + subs.getExpression(getProject())
344: + "' in '"
345: + f.getPath()
346: + "'"
347: + (byline ? " by line" : "")
348: + (flags.length() > 0 ? " with flags: '" + flags
349: + "'" : "") + ".", Project.MSG_VERBOSE);
350:
351: if (byline) {
352: StringBuffer linebuf = new StringBuffer();
353: String line = null;
354: String res = null;
355: int c;
356: boolean hasCR = false;
357:
358: do {
359: c = br.read();
360:
361: if (c == '\r') {
362: if (hasCR) {
363: // second CR -> EOL + possibly empty line
364: line = linebuf.toString();
365: res = doReplace(regex, subs, line, options);
366:
367: if (!res.equals(line)) {
368: changes = true;
369: }
370:
371: pw.print(res);
372: pw.print('\r');
373:
374: linebuf = new StringBuffer();
375: // hasCR is still true (for the second one)
376: } else {
377: // first CR in this line
378: hasCR = true;
379: }
380: } else if (c == '\n') {
381: // LF -> EOL
382: line = linebuf.toString();
383: res = doReplace(regex, subs, line, options);
384:
385: if (!res.equals(line)) {
386: changes = true;
387: }
388:
389: pw.print(res);
390: if (hasCR) {
391: pw.print('\r');
392: hasCR = false;
393: }
394: pw.print('\n');
395:
396: linebuf = new StringBuffer();
397: } else { // any other char
398: if ((hasCR) || (c < 0)) {
399: // Mac-style linebreak or EOF (or both)
400: line = linebuf.toString();
401: res = doReplace(regex, subs, line, options);
402:
403: if (!res.equals(line)) {
404: changes = true;
405: }
406:
407: pw.print(res);
408: if (hasCR) {
409: pw.print('\r');
410: hasCR = false;
411: }
412:
413: linebuf = new StringBuffer();
414: }
415:
416: if (c >= 0) {
417: linebuf.append((char) c);
418: }
419: }
420: } while (c >= 0);
421:
422: pw.flush();
423: } else {
424: String buf = FileUtils.readFully(br);
425: if (buf == null) {
426: buf = "";
427: }
428:
429: String res = doReplace(regex, subs, buf, options);
430:
431: if (!res.equals(buf)) {
432: changes = true;
433: }
434:
435: pw.print(res);
436: pw.flush();
437: }
438:
439: r.close();
440: r = null;
441: w.close();
442: w = null;
443:
444: if (changes) {
445: log("File has changed; saving the updated file",
446: Project.MSG_VERBOSE);
447: try {
448: FILE_UTILS.rename(temp, f);
449: temp = null;
450: } catch (IOException e) {
451: throw new BuildException(
452: "Couldn't rename temporary file " + temp,
453: getLocation());
454: }
455: } else {
456: log("No change made", Project.MSG_DEBUG);
457: }
458: } finally {
459: FileUtils.close(r);
460: FileUtils.close(w);
461: if (temp != null) {
462: temp.delete();
463: }
464: }
465: }
466:
467: /**
468: * Execute the task
469: *
470: * @throws BuildException is there is a problem in the task execution.
471: */
472: public void execute() throws BuildException {
473: if (regex == null) {
474: throw new BuildException("No expression to match.");
475: }
476: if (subs == null) {
477: throw new BuildException(
478: "Nothing to replace expression with.");
479: }
480:
481: if (file != null && filesets.size() > 0) {
482: throw new BuildException(
483: "You cannot supply the 'file' attribute "
484: + "and filesets at the same time.");
485: }
486:
487: int options = 0;
488:
489: if (flags.indexOf('g') != -1) {
490: options |= Regexp.REPLACE_ALL;
491: }
492:
493: if (flags.indexOf('i') != -1) {
494: options |= Regexp.MATCH_CASE_INSENSITIVE;
495: }
496:
497: if (flags.indexOf('m') != -1) {
498: options |= Regexp.MATCH_MULTILINE;
499: }
500:
501: if (flags.indexOf('s') != -1) {
502: options |= Regexp.MATCH_SINGLELINE;
503: }
504:
505: if (file != null && file.exists()) {
506: try {
507: doReplace(file, options);
508: } catch (IOException e) {
509: log(
510: "An error occurred processing file: '"
511: + file.getAbsolutePath() + "': "
512: + e.toString(), Project.MSG_ERR);
513: }
514: } else if (file != null) {
515: log("The following file is missing: '"
516: + file.getAbsolutePath() + "'", Project.MSG_ERR);
517: }
518:
519: int sz = filesets.size();
520:
521: for (int i = 0; i < sz; i++) {
522: FileSet fs = (FileSet) (filesets.elementAt(i));
523: DirectoryScanner ds = fs.getDirectoryScanner(getProject());
524:
525: String[] files = ds.getIncludedFiles();
526:
527: for (int j = 0; j < files.length; j++) {
528: File f = new File(fs.getDir(getProject()), files[j]);
529:
530: if (f.exists()) {
531: try {
532: doReplace(f, options);
533: } catch (Exception e) {
534: log("An error occurred processing file: '"
535: + f.getAbsolutePath() + "': "
536: + e.toString(), Project.MSG_ERR);
537: }
538: } else {
539: log("The following file is missing: '"
540: + f.getAbsolutePath() + "'",
541: Project.MSG_ERR);
542: }
543: }
544: }
545: }
546:
547: }
|