/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs.optional; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.charset.Charset; import java.nio.file.Files; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.RegularExpression; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.Substitution; import org.apache.tools.ant.types.resources.FileProvider; import org.apache.tools.ant.types.resources.Union; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.regexp.Regexp; import org.apache.tools.ant.util.regexp.RegexpUtil; /** * Performs regular expression string replacements in a text * file. The input file(s) must be able to be properly processed by * a Reader instance. That is, they must be text only, no binary. * * The syntax of the regular expression depends on the implementation that * you choose to use. The system property <code>ant.regexp.regexpimpl</code> * will be the classname of the implementation that will be used (the default * is <code>org.apache.tools.ant.util.regexp.JakartaOroRegexp</code> and * requires the Jakarta Oro Package). * * <pre> * Available implementations: * * org.apache.tools.ant.util.regexp.Jdk14RegexpRegexp (default) * Uses Java's built-in regular expression package * * org.apache.tools.ant.util.regexp.JakartaOroRegexp * Requires the jakarta-oro package * * org.apache.tools.ant.util.regexp.JakartaRegexpRegexp * Requires the jakarta-regexp package * * Usage: * * Call Syntax: * * <replaceregexp file="file" * match="pattern" * replace="pattern" * flags="options"? * byline="true|false"? > * regexp? * substitution? * fileset* * </replaceregexp> * * NOTE: You must have either the file attribute specified, or at least one fileset subelement * to operation on. You may not have the file attribute specified if you nest fileset elements * inside this task. Also, you cannot specify both match and a regular expression subelement at * the same time, nor can you specify the replace attribute and the substitution subelement at * the same time. * * Attributes: * * file --> A single file to operation on (mutually exclusive * with the fileset subelements) * match --> The Regular expression to match * replace --> The Expression replacement string * flags --> The options to give to the replacement * g = Substitute all occurrences. default is to replace only the first one * i = Case insensitive match * * byline --> Should this file be processed a single line at a time (default is false) * "true" indicates to perform replacement on a line by line basis * "false" indicates to perform replacement on the whole file at once. * * Example: * * The following call could be used to replace an old property name in a ".properties" * file with a new name. In the replace attribute, you can refer to any part of the * match expression in parenthesis using backslash followed by a number like '\1'. * * <replaceregexp file="test.properties" * match="MyProperty=(.*)" * replace="NewProperty=\1" * byline="true" /> * * </pre> * */ public class ReplaceRegExp extends Task { private File file; private String flags; private boolean byline; private Union resources; private RegularExpression regex; private Substitution subs; private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); private boolean preserveLastModified = false; /** * Encoding to assume for the files */ private String encoding = null; /** Default Constructor */ public ReplaceRegExp() { super(); this.file = null; this.flags = ""; this.byline = false; this.regex = null; this.subs = null; } /** * file for which the regular expression should be replaced; * required unless a nested fileset is supplied. * @param file The file for which the reg exp should be replaced. */ public void setFile(File file) { this.file = file; } /** * the regular expression pattern to match in the file(s); * required if no nested <regexp> is used * @param match the match attribute. */ public void setMatch(String match) { if (regex != null) { throw new BuildException("Only one regular expression is allowed"); } regex = new RegularExpression(); regex.setPattern(match); } /** * The substitution pattern to place in the file(s) in place * of the regular expression. * Required if no nested <substitution> is used * @param replace the replace attribute */ public void setReplace(String replace) { if (subs != null) { throw new BuildException( "Only one substitution expression is allowed"); } subs = new Substitution(); subs.setExpression(replace); } /** * The flags to use when matching the regular expression. For more * information, consult the Perl5 syntax. * <ul> * <li>g : Global replacement. Replace all occurrences found * <li>i : Case Insensitive. Do not consider case in the match * <li>m : Multiline. Treat the string as multiple lines of input, * using "^" and "$" as the start or end of any line, respectively, * rather than start or end of string. * <li> s : Singleline. Treat the string as a single line of input, using * "." to match any character, including a newline, which normally, * it would not match. *</ul> * @param flags the flags attribute */ public void setFlags(String flags) { this.flags = flags; } /** * Process the file(s) one line at a time, executing the replacement * on one line at a time. This is useful if you * want to only replace the first occurrence of a regular expression on * each line, which is not easy to do when processing the file as a whole. * Defaults to <i>false</i>. * * @param byline the byline attribute as a string * @deprecated since 1.6.x. * Use setByLine(boolean). */ @Deprecated public void setByLine(String byline) { this.byline = Boolean.parseBoolean(byline); } /** * Process the file(s) one line at a time, executing the replacement * on one line at a time. This is useful if you * want to only replace the first occurrence of a regular expression on * each line, which is not easy to do when processing the file as a whole. * Defaults to <i>false</i>. * * @param byline the byline attribute */ public void setByLine(boolean byline) { this.byline = byline; } /** * Specifies the encoding Ant expects the files to be in - * defaults to the platforms default encoding. * @param encoding the encoding attribute * * @since Ant 1.6 */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * list files to apply the replacement to * @param set the fileset element */ public void addFileset(FileSet set) { addConfigured(set); } /** * Support arbitrary file system based resource collections. * * @since Ant 1.8.0 */ public void addConfigured(ResourceCollection rc) { if (!rc.isFilesystemOnly()) { throw new BuildException("only filesystem resources are supported"); } if (resources == null) { resources = new Union(); } resources.add(rc); } /** * A regular expression. * You can use this element to refer to a previously * defined regular expression datatype instance * @return the regular expression object to be configured as an element */ public RegularExpression createRegexp() { if (regex != null) { throw new BuildException("Only one regular expression is allowed."); } regex = new RegularExpression(); return regex; } /** * A substitution pattern. You can use this element to refer to a previously * defined substitution pattern datatype instance. * @return the substitution pattern object to be configured as an element */ public Substitution createSubstitution() { if (subs != null) { throw new BuildException( "Only one substitution expression is allowed"); } subs = new Substitution(); return subs; } /** * Whether the file timestamp shall be preserved even if the file * is modified. * * @since Ant 1.8.0 */ public void setPreserveLastModified(boolean b) { preserveLastModified = b; } /** * Invoke a regular expression (r) on a string (input) using * substitutions (s) for a matching regex. * * @param r a regular expression * @param s a Substitution * @param input the string to do the replacement on * @param options The options for the regular expression * @return the replacement result */ protected String doReplace(RegularExpression r, Substitution s, String input, int options) { String res = input; Regexp regexp = r.getRegexp(getProject()); if (regexp.matches(input, options)) { log("Found match; substituting", Project.MSG_DEBUG); res = regexp.substitute(input, s.getExpression(getProject()), options); } return res; } /** * Perform the replacement on a file * * @param f the file to perform the replacement on * @param options the regular expressions options * @exception IOException if an error occurs */ protected void doReplace(File f, int options) throws IOException { File temp = FILE_UTILS.createTempFile("replace", ".txt", null, true, true); try { boolean changes = false; final Charset charset = encoding == null ? Charset.defaultCharset() : Charset.forName(encoding); try (InputStream is = Files.newInputStream(f.toPath()); OutputStream os = Files.newOutputStream(temp.toPath())) { Reader r = null; Writer w = null; try { r = new InputStreamReader(is, charset); w = new OutputStreamWriter(os, charset); log("Replacing pattern '" + regex.getPattern(getProject()) + "' with '" + subs.getExpression(getProject()) + "' in '" + f.getPath() + "'" + (byline ? " by line" : "") + (flags.length() > 0 ? " with flags: '" + flags + "'" : "") + ".", Project.MSG_VERBOSE); if (byline) { r = new BufferedReader(r); w = new BufferedWriter(w); StringBuilder linebuf = new StringBuilder(); int c; boolean hasCR = false; do { c = r.read(); if (c == '\r') { if (hasCR) { // second CR -> EOL + possibly empty line changes |= replaceAndWrite(linebuf.toString(), w, options); w.write('\r'); linebuf = new StringBuilder(); // hasCR is still true (for the second one) } else { // first CR in this line hasCR = true; } } else if (c == '\n') { // LF -> EOL changes |= replaceAndWrite(linebuf.toString(), w, options); if (hasCR) { w.write('\r'); hasCR = false; } w.write('\n'); linebuf = new StringBuilder(); } else { // any other char if ((hasCR) || (c < 0)) { // Mac-style linebreak or EOF (or both) changes |= replaceAndWrite(linebuf.toString(), w, options); if (hasCR) { w.write('\r'); hasCR = false; } linebuf = new StringBuilder(); } if (c >= 0) { linebuf.append((char) c); } } } while (c >= 0); } else { changes = multilineReplace(r, w, options); } } finally { FileUtils.close(r); FileUtils.close(w); } } if (changes) { log("File has changed; saving the updated file", Project.MSG_VERBOSE); try { long origLastModified = f.lastModified(); FILE_UTILS.rename(temp, f); if (preserveLastModified) { FILE_UTILS.setFileLastModified(f, origLastModified); } temp = null; } catch (IOException e) { throw new BuildException("Couldn't rename temporary file " + temp, e, getLocation()); } } else { log("No change made", Project.MSG_DEBUG); } } finally { if (temp != null) { temp.delete(); } } } /** * Execute the task * * @throws BuildException is there is a problem in the task execution. */ @Override public void execute() throws BuildException { if (regex == null) { throw new BuildException("No expression to match."); } if (subs == null) { throw new BuildException("Nothing to replace expression with."); } if (file != null && resources != null) { throw new BuildException( "You cannot supply the 'file' attribute and resource collections at the same time."); } int options = RegexpUtil.asOptions(flags); if (file != null && file.exists()) { try { doReplace(file, options); } catch (IOException e) { log("An error occurred processing file: '" + file.getAbsolutePath() + "': " + e.toString(), Project.MSG_ERR); } } else if (file != null) { log("The following file is missing: '" + file.getAbsolutePath() + "'", Project.MSG_ERR); } if (resources != null) { for (Resource r : resources) { File f = r.as(FileProvider.class).getFile(); if (f.exists()) { try { doReplace(f, options); } catch (Exception e) { log("An error occurred processing file: '" + f.getAbsolutePath() + "': " + e.toString(), Project.MSG_ERR); } } else { log("The following file is missing: '" + f.getAbsolutePath() + "'", Project.MSG_ERR); } } } } private boolean multilineReplace(Reader r, Writer w, int options) throws IOException { return replaceAndWrite(FileUtils.safeReadFully(r), w, options); } private boolean replaceAndWrite(String s, Writer w, int options) throws IOException { String res = doReplace(regex, subs, s, options); w.write(res); return !res.equals(s); } }