content: /*
   *  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
   *
   *      https://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:
   *
   *     &lt;replaceregexp file="file"
   *                    match="pattern"
   *                    replace="pattern"
   *                    flags="options"?
   *                    byline="true|false"?
   *                    failOnError="true|false"? &gt;
   *       regexp?
   *       substitution?
   *       fileset*
   *     &lt;/replaceregexp&gt;
   *
   *    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    --&gt; A single file to operation on (mutually exclusive
   *                    with the fileset subelements)
   *     match   --&gt; The Regular expression to match
   *     replace --&gt; The Expression replacement string
   *     flags   --&gt; The options to give to the replacement
   *                 g = Substitute all occurrences. default is to replace only the first one
   *                 i = Case insensitive match
   *
   *     byline  --&gt; 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.
   *
   *     failOnError --&gt; Should this task fail if an error occurs (default is false)
   *                 "true" indicates that this task should fail if an error occurs
   *                 "false" indicates that this task should continue if an error occurs
   *
   *  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'.
   *
   *     &lt;replaceregexp file="test.properties"
   *                    match="MyProperty=(.*)"
   *                    replace="NewProperty=\1"
   *                    byline="true" /&gt;
   *
   * </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 boolean failonerror = false;
  
      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 &lt;regexp&gt; 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 &lt;substitution&gt; 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>
       *  <li>i : Case Insensitive.  Do not consider case in the match</li>
       *  <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>
       *  <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.</li>
       * </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.
       *
       * @param rc ResourceCollection
       * @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.
       *
       * @param b boolean
       * @since Ant 1.8.0
       */
      public void setPreserveLastModified(boolean b) {
          preserveLastModified = b;
      }
  
      /**
       * If false, note errors but continue.
       *
       * @param failonerror true or false
       * @since Ant 1.10.15
       */
      public void setFailOnError(boolean failonerror) {
          this.failonerror = failonerror;
      }
  
      /**
       * 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(getProject(), "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.isEmpty() ? "" : " 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, true);
                      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);
                  if (failonerror) {
                      throw new BuildException("An error occurred processing file: '" + file.getAbsolutePath() + "'",
                                               e, getLocation());
                  }
              }
          } else if (file != null) {
              log("The following file is missing: '"
                  + file.getAbsolutePath() + "'", Project.MSG_ERR);
              if (failonerror) {
                  throw new BuildException("The following file is missing: '" + file.getAbsolutePath() + "'");
              }
          }
  
          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);
                          if (failonerror) {
                              throw new BuildException("An error occurred processing file: '" + f.getAbsolutePath() + "'",
                                                       e, getLocation());
                          }
                      }
                  } else {
                      log("The following file is missing: '"
                          + f.getAbsolutePath() + "'", Project.MSG_ERR);
                      if (failonerror) {
                          throw new BuildException("The following file is missing: '" + f.getAbsolutePath() + "'");
                      }
                  }
              }
          }
      }
  
      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);
      }
  }
  