Clover coverage report - Ant Coverage
Coverage timestamp: Tue Apr 8 2003 20:43:55 EST
file stats: LOC: 663   Methods: 28
NCLOC: 351   Classes: 3
 
 Source file Conditionals Statements Methods TOTAL
Replace.java 51.3% 59.3% 57.1% 56.8%
 1   
 /*
 2   
  * The Apache Software License, Version 1.1
 3   
  *
 4   
  * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights
 5   
  * reserved.
 6   
  *
 7   
  * Redistribution and use in source and binary forms, with or without
 8   
  * modification, are permitted provided that the following conditions
 9   
  * are met:
 10   
  *
 11   
  * 1. Redistributions of source code must retain the above copyright
 12   
  *    notice, this list of conditions and the following disclaimer.
 13   
  *
 14   
  * 2. Redistributions in binary form must reproduce the above copyright
 15   
  *    notice, this list of conditions and the following disclaimer in
 16   
  *    the documentation and/or other materials provided with the
 17   
  *    distribution.
 18   
  *
 19   
  * 3. The end-user documentation included with the redistribution, if
 20   
  *    any, must include the following acknowlegement:
 21   
  *       "This product includes software developed by the
 22   
  *        Apache Software Foundation (http://www.apache.org/)."
 23   
  *    Alternately, this acknowlegement may appear in the software itself,
 24   
  *    if and wherever such third-party acknowlegements normally appear.
 25   
  *
 26   
  * 4. The names "Ant" and "Apache Software
 27   
  *    Foundation" must not be used to endorse or promote products derived
 28   
  *    from this software without prior written permission. For written
 29   
  *    permission, please contact apache@apache.org.
 30   
  *
 31   
  * 5. Products derived from this software may not be called "Apache"
 32   
  *    nor may "Apache" appear in their names without prior written
 33   
  *    permission of the Apache Group.
 34   
  *
 35   
  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 36   
  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 37   
  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 38   
  * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 39   
  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 40   
  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 41   
  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 42   
  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 43   
  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 44   
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 45   
  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 46   
  * SUCH DAMAGE.
 47   
  * ====================================================================
 48   
  *
 49   
  * This software consists of voluntary contributions made by many
 50   
  * individuals on behalf of the Apache Software Foundation.  For more
 51   
  * information on the Apache Software Foundation, please see
 52   
  * <http://www.apache.org/>.
 53   
  */
 54   
 
 55   
 package org.apache.tools.ant.taskdefs;
 56   
 
 57   
 import java.io.BufferedReader;
 58   
 import java.io.BufferedWriter;
 59   
 import java.io.File;
 60   
 import java.io.FileInputStream;
 61   
 import java.io.FileNotFoundException;
 62   
 import java.io.FileOutputStream;
 63   
 import java.io.FileReader;
 64   
 import java.io.FileWriter;
 65   
 import java.io.IOException;
 66   
 import java.io.InputStreamReader;
 67   
 import java.io.OutputStreamWriter;
 68   
 import java.io.Reader;
 69   
 import java.io.Writer;
 70   
 import java.util.Enumeration;
 71   
 import java.util.Properties;
 72   
 import java.util.Vector;
 73   
 import org.apache.tools.ant.BuildException;
 74   
 import org.apache.tools.ant.DirectoryScanner;
 75   
 import org.apache.tools.ant.Project;
 76   
 import org.apache.tools.ant.util.FileUtils;
 77   
 import org.apache.tools.ant.util.StringUtils;
 78   
 
 79   
 /**
 80   
  * Replaces all occurrences of one or more string tokens with given
 81   
  * values in the indicated files. Each value can be either a string 
 82   
  * or the value of a property available in a designated property file.
 83   
  * If you want to replace a text that crosses line boundaries, you
 84   
  * must use a nested <code>&lt;replacetoken&gt;</code> element.
 85   
  * @author Stefano Mazzocchi 
 86   
  *         <a href="mailto:stefano@apache.org">stefano@apache.org</a>
 87   
  * @author <a href="mailto:erik@desknetinc.com">Erik Langenbach</a>
 88   
  *
 89   
  * @since Ant 1.1
 90   
  *
 91   
  * @ant.task category="filesystem"
 92   
  */
 93   
 public class Replace extends MatchingTask {
 94   
     
 95   
     private File src = null;
 96   
     private NestedString token = null;
 97   
     private NestedString value = new NestedString();
 98   
 
 99   
     private File propertyFile = null;
 100   
     private File replaceFilterFile = null;
 101   
     private Properties properties = null;
 102   
     private Vector replacefilters = new Vector();
 103   
 
 104   
     private File dir = null;
 105   
 
 106   
     private int fileCount;
 107   
     private int replaceCount;    
 108   
     private boolean summary = false;
 109   
     
 110   
     /** The encoding used to read and write files - if null, uses default */
 111   
     private String encoding = null;
 112   
     
 113   
     private FileUtils fileUtils = FileUtils.newFileUtils();
 114   
 
 115   
     /**
 116   
      * an inline string to use as the replacement text
 117   
      */
 118   
     public class NestedString {
 119   
 
 120   
         private StringBuffer buf = new StringBuffer();
 121   
 
 122  2
         public void addText(String val) {
 123  2
             buf.append(val);
 124   
         }
 125   
 
 126  8
         public String getText() {
 127  8
             return buf.substring(0);
 128   
         }
 129   
     }
 130   
 
 131   
     /**
 132   
      * A filter to apply.
 133   
      */
 134   
     public class Replacefilter {
 135   
         private String token;
 136   
         private String value;
 137   
         private String property;
 138   
 
 139   
         /**
 140   
          * validate the filter's configuration
 141   
          * @throws BuildException if any part is invalid
 142   
          */
 143  3
         public void validate() throws BuildException {
 144   
             //Validate mandatory attributes
 145  3
             if (token == null) {
 146  1
                 String message = "token is a mandatory attribute " 
 147   
                     + "of replacefilter.";
 148  1
                 throw new BuildException(message);
 149   
             }
 150   
 
 151  2
             if ("".equals(token)) {
 152  1
                 String message = "The token attribute must not be an empty "
 153   
                     + "string.";
 154  1
                 throw new BuildException(message);
 155   
             }
 156   
 
 157   
             //value and property are mutually exclusive attributes
 158  1
             if ((value != null) && (property != null)) {
 159  0
                 String message = "Either value or property " 
 160   
                     + "can be specified, but a replacefilter " 
 161   
                     + "element cannot have both.";
 162  0
                 throw new BuildException(message);
 163   
             }
 164   
 
 165  1
             if ((property != null)) {
 166   
                 //the property attribute must have access to a property file
 167  0
                 if (propertyFile == null) {
 168  0
                     String message = "The replacefilter's property attribute "
 169   
                         + "can only be used with the replacetask's "
 170   
                         + "propertyFile attribute.";
 171  0
                     throw new BuildException(message);
 172   
                 }
 173   
 
 174   
                 //Make sure property exists in property file
 175  0
                 if (properties == null ||
 176   
                     properties.getProperty(property) == null) {
 177  0
                     String message = "property \"" + property 
 178   
                         + "\" was not found in " + propertyFile.getPath();
 179  0
                     throw new BuildException(message);
 180   
                 }
 181   
             }
 182   
         }
 183   
 
 184   
         /**
 185   
          * Get the replacement value for this filter token.
 186   
          */
 187  2
         public String getReplaceValue() {
 188  2
             if (property != null) {
 189  0
                 return properties.getProperty(property);
 190  2
             } else if (value != null) {
 191  0
                 return value;
 192  2
             } else if (Replace.this.value != null) {
 193  2
                 return Replace.this.value.getText();
 194   
             } else {
 195   
                 //Default is empty string
 196  0
                 return new String("");
 197   
             }
 198   
         }
 199   
 
 200   
         /**
 201   
          * Set the token to replace
 202   
          * @param token token
 203   
          */
 204  2
         public void setToken(String token) {
 205  2
             this.token = token;
 206   
         }
 207   
 
 208   
         /**
 209   
          * Get the string to search for
 210   
          * @return current token
 211   
          */
 212  2
         public String getToken() {
 213  2
             return token;
 214   
         }
 215   
 
 216   
         /**
 217   
          * The replacement string; required if <code>property<code>
 218   
          * is not set
 219   
          * @param value value to replace
 220   
          */
 221  0
         public void setValue(String value) {
 222  0
             this.value = value;
 223   
         }
 224   
 
 225   
         /**
 226   
          * Get replacements string
 227   
          * @return replacement or null
 228   
          */
 229  0
         public String getValue() {
 230  0
             return value;
 231   
         }
 232   
 
 233   
         /**
 234   
          * Set the name of the property whose value is to serve as
 235   
          * the replacement value; required if <code>value</code> is not set.
 236   
          * @param property propname
 237   
          */
 238  0
         public void setProperty(String property) {
 239  0
             this.property = property;
 240   
         }
 241   
 
 242   
         /**
 243   
          * Get the name of the property whose value is to serve as
 244   
          * the replacement value;
 245   
          * @return property or null
 246   
          */
 247  0
         public String getProperty() {
 248  0
             return property;
 249   
         }
 250   
     }
 251   
 
 252   
     /**
 253   
      * Do the execution.
 254   
      * @throws BuildException if we cant build
 255   
      */
 256  8
     public void execute() throws BuildException {
 257   
 
 258  8
         Vector savedFilters = (Vector) replacefilters.clone();
 259  8
         Properties savedProperties = 
 260   
             properties == null ? null : (Properties) properties.clone();
 261   
 
 262  8
         try {
 263  8
             if (replaceFilterFile != null) {
 264  0
                 Properties props = getProperties(replaceFilterFile);
 265  0
                 Enumeration enum = props.keys();
 266  0
                 while (enum.hasMoreElements()){
 267  0
                     String token =  enum.nextElement().toString();
 268  0
                     Replacefilter replaceFilter = createReplacefilter();
 269  0
                     replaceFilter.setToken(token);
 270  0
                     replaceFilter.setValue(props.getProperty(token));
 271   
                 }
 272   
             }
 273   
             
 274  8
             validateAttributes();
 275   
             
 276  4
             if (propertyFile != null) {
 277  0
                 properties = getProperties(propertyFile);
 278   
             }
 279   
             
 280  4
             validateReplacefilters();
 281  2
             fileCount = 0;
 282  2
             replaceCount = 0;
 283   
             
 284  2
             if (src != null) {
 285  2
                 processFile(src);
 286   
             }
 287   
             
 288  2
             if (dir != null) {
 289  0
                 DirectoryScanner ds = super.getDirectoryScanner(dir);
 290  0
                 String[] srcs = ds.getIncludedFiles();
 291   
                 
 292  0
                 for (int i = 0; i < srcs.length; i++) {
 293  0
                     File file = new File(dir, srcs[i]);
 294  0
                     processFile(file);
 295   
                 }
 296   
             }
 297   
             
 298  2
             if (summary) {
 299  0
                 log("Replaced " + replaceCount + " occurrences in " 
 300   
                     + fileCount + " files.", Project.MSG_INFO);
 301   
             }
 302   
         } finally {
 303  8
             replacefilters = savedFilters;
 304  8
             properties = savedProperties;
 305   
         } // end of finally
 306   
         
 307   
     }
 308   
     
 309   
     /**
 310   
      * Validate attributes provided for this task in .xml build file.
 311   
      *
 312   
      * @exception BuildException if any supplied attribute is invalid or any
 313   
      * mandatory attribute is missing
 314   
      */
 315  8
     public void validateAttributes() throws BuildException {
 316  8
         if (src == null && dir == null) {
 317  1
             String message = "Either the file or the dir attribute " 
 318   
                 + "must be specified";
 319  1
             throw new BuildException(message, getLocation());
 320   
         }
 321  7
         if (propertyFile != null && !propertyFile.exists()) {
 322  0
             String message = "Property file " + propertyFile.getPath() 
 323   
                 + " does not exist.";
 324  0
             throw new BuildException(message, getLocation());
 325   
         }
 326  7
         if (token == null && replacefilters.size() == 0) {
 327  2
             String message = "Either token or a nested replacefilter "
 328   
                 + "must be specified";
 329  2
             throw new BuildException(message, getLocation());
 330   
         }
 331  5
         if (token != null && "".equals(token.getText())) {
 332  1
             String message = "The token attribute must not be an empty string.";
 333  1
             throw new BuildException(message, getLocation());
 334   
         }
 335   
     }
 336   
 
 337   
     /**
 338   
      * Validate nested elements.
 339   
      *
 340   
      * @exception BuildException if any supplied attribute is invalid or any
 341   
      * mandatory attribute is missing
 342   
      */
 343  4
     public void validateReplacefilters()
 344   
             throws BuildException {
 345  4
         for (int i = 0; i < replacefilters.size(); i++) {
 346  3
             Replacefilter element = 
 347   
                 (Replacefilter) replacefilters.elementAt(i);
 348  3
             element.validate();
 349   
         }
 350   
     }
 351   
 
 352   
     /**
 353   
      * helper method to load a properties file and throw a build exception
 354   
      * if it cannot be loaded
 355   
      * @param propertyFile
 356   
      * @return loaded properties collection
 357   
      * @throws BuildException if the file could not be found or read
 358   
      */
 359  0
     public Properties getProperties(File propertyFile) throws BuildException {
 360  0
         Properties properties = new Properties();
 361   
 
 362  0
         try {
 363  0
             properties.load(new FileInputStream(propertyFile));
 364   
         } catch (FileNotFoundException e) {
 365  0
             String message = "Property file (" + propertyFile.getPath() 
 366   
                 + ") not found.";
 367  0
             throw new BuildException(message);
 368   
         } catch (IOException e) {
 369  0
             String message = "Property file (" + propertyFile.getPath() 
 370   
                 + ") cannot be loaded.";
 371  0
             throw new BuildException(message);
 372   
         }
 373   
 
 374  0
         return properties;
 375   
     }
 376   
 
 377   
     /**
 378   
      * Perform the replacement on the given file.
 379   
      *
 380   
      * The replacement is performed on a temporary file which then
 381   
      * replaces the original file.
 382   
      *
 383   
      * @param src the source file
 384   
      */
 385  2
     private void processFile(File src) throws BuildException {
 386  2
         if (!src.exists()) {
 387  0
             throw new BuildException("Replace: source file " + src.getPath() 
 388   
                                      + " doesn't exist", getLocation());
 389   
         }
 390   
 
 391  2
         File temp = fileUtils.createTempFile("rep", ".tmp", 
 392   
                                              fileUtils.getParentFile(src));
 393   
 
 394  2
         Reader reader = null;
 395  2
         Writer writer = null;
 396  2
         try {
 397  2
             reader = encoding == null ? new FileReader(src)
 398   
                 : new InputStreamReader(new FileInputStream(src), encoding);
 399  2
             writer = encoding == null ? new FileWriter(temp)
 400   
                 : new OutputStreamWriter(new FileOutputStream(temp), encoding);
 401   
             
 402  2
             BufferedReader br = new BufferedReader(reader);
 403  2
             BufferedWriter bw = new BufferedWriter(writer);
 404   
 
 405   
             // read the entire file into a StringBuffer
 406   
             //   size of work buffer may be bigger than needed
 407   
             //   when multibyte characters exist in the source file
 408   
             //   but then again, it might be smaller than needed on
 409   
             //   platforms like Windows where length can't be trusted
 410  2
             int fileLengthInBytes = (int) src.length();
 411  2
             StringBuffer tmpBuf = new StringBuffer(fileLengthInBytes);
 412  2
             int readChar = 0;
 413  2
             int totread = 0;
 414  2
             while (true) {
 415  2
                 readChar = br.read();
 416  2
                 if (readChar < 0) { break; }
 417  0
                 tmpBuf.append((char) readChar);
 418  0
                 totread++;
 419   
             }
 420   
 
 421   
             // create a String so we can use indexOf
 422  2
             String buf = tmpBuf.toString();
 423   
 
 424   
             //Preserve original string (buf) so we can compare the result
 425  2
             String newString = new String(buf);
 426   
 
 427  2
             if (token != null) {
 428   
                 // line separators in values and tokens are "\n"
 429   
                 // in order to compare with the file contents, replace them
 430   
                 // as needed
 431  1
                 String val = stringReplace(value.getText(), "\n",
 432   
                                            StringUtils.LINE_SEP, false);
 433  1
                 String tok = stringReplace(token.getText(), "\n",
 434   
                                            StringUtils.LINE_SEP, false);
 435   
                 
 436   
                 // for each found token, replace with value
 437  1
                 log("Replacing in " + src.getPath() + ": " + token.getText() 
 438   
                     + " --> " + value.getText(), Project.MSG_VERBOSE);
 439  1
                 newString = stringReplace(newString, tok, val, true);
 440   
             }
 441   
 
 442  2
             if (replacefilters.size() > 0) {
 443  1
                 newString = processReplacefilters(newString, src.getPath());
 444   
             }
 445   
 
 446  2
             boolean changes = !newString.equals(buf);
 447  2
             if (changes) {
 448  0
                 bw.write(newString, 0, newString.length());
 449  0
                 bw.flush();
 450   
             }
 451   
 
 452   
             // cleanup
 453  2
             bw.close();
 454  2
             writer = null;
 455  2
             br.close();
 456  2
             reader = null;
 457   
 
 458   
             // If there were changes, move the new one to the old one;
 459   
             // otherwise, delete the new one
 460  2
             if (changes) {
 461  0
                 ++fileCount;
 462  0
                 if (!src.delete()) {
 463  0
                     throw new BuildException("Couldn't delete " + src,
 464   
                                              getLocation());
 465   
                 }
 466  0
                 if (!temp.renameTo(src)) {
 467  0
                     throw new BuildException("Couldn't rename temporary file " 
 468   
                                              + temp, getLocation());
 469   
                 }
 470  0
                 temp = null;
 471   
             }
 472   
         } catch (IOException ioe) {
 473  0
             throw new BuildException("IOException in " + src + " - " + 
 474   
                                      ioe.getClass().getName() + ":" 
 475   
                                      + ioe.getMessage(), ioe, getLocation());
 476   
         } finally {
 477  2
             if (reader != null) {
 478  0
                 try {
 479  0
                     reader.close();
 480   
                 } catch (IOException e) {}
 481   
             }
 482  2
             if (writer != null) {
 483  0
                 try {
 484  0
                     writer.close();
 485   
                 } catch (IOException e) {}
 486   
             }
 487  2
             if (temp != null) {
 488  2
                 temp.delete();
 489   
             }
 490   
         }
 491   
         
 492   
     }
 493   
 
 494   
     /**
 495   
      * apply all replace filters to a buffer
 496   
      * @param buffer string to filter
 497   
      * @param filename filename for logging purposes
 498   
      * @return filtered string
 499   
      */
 500  1
     private String processReplacefilters(String buffer, String filename) {
 501  1
         String newString = new String(buffer);
 502   
 
 503  1
         for (int i = 0; i < replacefilters.size(); i++) {
 504  1
             Replacefilter filter = (Replacefilter) replacefilters.elementAt(i);
 505   
 
 506   
             //for each found token, replace with value
 507  1
             log("Replacing in " + filename + ": " + filter.getToken() 
 508   
                 + " --> " + filter.getReplaceValue(), Project.MSG_VERBOSE);
 509  1
             newString = stringReplace(newString, filter.getToken(), 
 510   
                                       filter.getReplaceValue(), true);
 511   
         }
 512   
 
 513  1
         return newString;
 514   
     }
 515   
 
 516   
 
 517   
     /**
 518   
      * Set the source file; required unless <code>dir</code> is set.
 519   
      * @param file source file
 520   
      */
 521  7
     public void setFile(File file) {
 522  7
         this.src = file;
 523   
     }
 524   
 
 525   
     /**
 526   
      * Indicates whether a summary of the replace operation should be
 527   
      * produced, detailing how many token occurrences and files were
 528   
      * processed; optional, default=false
 529   
      *
 530   
      * @param summary true if you would like a summary logged of the
 531   
      * replace operation
 532   
      */
 533  0
     public void setSummary(boolean summary) {
 534  0
         this.summary = summary;
 535   
     }
 536   
     
 537   
     
 538   
     /**
 539   
      * Sets the name of a property file containing filters; optional.
 540   
      * Each property will be treated as a
 541   
      * replacefilter where token is the name of the property and value
 542   
      * is the value of the property.
 543   
      * @param filename file to load
 544   
      */
 545  0
     public void setReplaceFilterFile(File filename) {
 546  0
         replaceFilterFile = filename;
 547   
     }
 548   
 
 549   
     /**
 550   
      * The base directory to use when replacing a token in multiple files;
 551   
      * required if <code>file</code> is not defined.
 552   
      * @param dir base dir
 553   
      */
 554  0
     public void setDir(File dir) {
 555  0
         this.dir = dir;
 556   
     }
 557   
 
 558   
     /**
 559   
      * Set the string token to replace;
 560   
      * required unless a nested
 561   
      * <code>replacetoken</code> element or the <code>replacefilterfile</code>
 562   
      * attribute is used.
 563   
      * @param token token string
 564   
      */
 565  2
     public void setToken(String token) {
 566  2
         createReplaceToken().addText(token);
 567   
     }
 568   
 
 569   
     /**
 570   
      * Set the string value to use as token replacement;
 571   
      * optional, default is the empty string ""
 572   
      * @param value replacement value
 573   
      */
 574  0
     public void setValue(String value) {
 575  0
         createReplaceValue().addText(value);
 576   
     }
 577   
 
 578   
     /**
 579   
      * Set the file encoding to use on the files read and written by the task;
 580   
      * optional, defaults to default JVM encoding
 581   
      *
 582   
      * @param encoding the encoding to use on the files
 583   
      */
 584  0
     public void setEncoding(String encoding) {
 585  0
         this.encoding = encoding;
 586   
     }
 587   
     
 588   
     /**
 589   
      * the token to filter as the text of a nested element
 590   
      * @return nested token to configure
 591   
      */
 592  2
     public NestedString createReplaceToken() {
 593  2
         if (token == null) {
 594  2
             token = new NestedString();
 595   
         }
 596  2
         return token;
 597   
     }
 598   
 
 599   
     /**
 600   
      * the string to replace the token as the text of a nested element
 601   
      * @return replacement value to configure
 602   
      */
 603  0
     public NestedString createReplaceValue() {
 604  0
         return value;
 605   
     }
 606   
 
 607   
     /**
 608   
      * The name of a property file from which properties specified using
 609   
      * nested <code>&lt;replacefilter&gt;</code> elements are drawn;
 610   
      * Required only if <i>property</i> attribute of
 611   
      * <code>&lt;replacefilter&gt;</code> is used.
 612   
      * @param filename file to load
 613   
      */
 614  0
     public void setPropertyFile(File filename) {
 615  0
         propertyFile = filename;
 616   
     }
 617   
 
 618   
     /**
 619   
      * Add a nested &lt;replacefilter&gt; element.
 620   
      */
 621  3
     public Replacefilter createReplacefilter() {
 622  3
         Replacefilter filter = new Replacefilter();
 623  3
         replacefilters.addElement(filter);
 624  3
         return filter;
 625   
     }
 626   
 
 627   
     /**
 628   
      * Replace occurrences of str1 in string str with str2
 629   
      */    
 630  4
     private String stringReplace(String str, String str1, String str2,
 631   
                                  boolean countReplaces) {
 632  4
         StringBuffer ret = new StringBuffer();
 633  4
         int start = 0;
 634  4
         int found = str.indexOf(str1);
 635  4
         while (found >= 0) {
 636   
             // write everything up to the found str1
 637  0
             if (found > start) {
 638  0
                 ret.append(str.substring(start, found));
 639   
             }
 640   
 
 641   
             // write the replacement str2
 642  0
             if (str2 != null) {
 643  0
                 ret.append(str2);
 644   
             }
 645   
 
 646   
             // search again
 647  0
             start = found + str1.length();
 648  0
             found = str.indexOf(str1, start);
 649  0
             if (countReplaces) {
 650  0
                 ++replaceCount;
 651   
             }
 652   
         }
 653   
 
 654   
         // write the remaining characters
 655  4
         if (str.length() > start) {
 656  1
             ret.append(str.substring(start, str.length()));
 657   
         }
 658   
 
 659  4
         return ret.toString();
 660   
     }
 661   
 
 662   
 }
 663