org.apache.tools.ant.taskdefs.Replace Maven / Gradle / Ivy
The newest version!
/*
* 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;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.Union;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.StringUtils;
/**
* Replaces all occurrences of one or more string tokens with given
* values in the indicated files. Each value can be either a string
* or the value of a property available in a designated property file.
* If you want to replace a text that crosses line boundaries, you
* must use a nested <replacetoken>
element.
*
* @since Ant 1.1
*
* @ant.task category="filesystem"
*/
public class Replace extends MatchingTask {
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
private File sourceFile = null;
private NestedString token = null;
private NestedString value = new NestedString();
private Resource propertyResource = null;
private Resource replaceFilterResource = null;
private Properties properties = null;
private ArrayList replacefilters = new ArrayList();
private File dir = null;
private int fileCount;
private int replaceCount;
private boolean summary = false;
/** The encoding used to read and write files - if null, uses default */
private String encoding = null;
private Union resources;
private boolean preserveLastModified = false;
private boolean failOnNoReplacements = false;
/**
* An inline string to use as the replacement text.
*/
public class NestedString {
private boolean expandProperties = false;
private StringBuffer buf = new StringBuffer();
/**
* Whether properties should be expanded in nested test.
*
* If you use this class via its Java interface the text
* you add via {@link #addText addText} has most likely been
* expanded already so you do not want to set this to
* true.
*
* @since Ant 1.8.0
*/
public void setExpandProperties(boolean b) {
expandProperties = b;
}
/**
* The text of the element.
*
* @param val the string to add
*/
public void addText(String val) {
buf.append(val);
}
/**
* @return the text
*/
public String getText() {
String s = buf.toString();
return expandProperties ? getProject().replaceProperties(s) : s;
}
}
/**
* A filter to apply.
*/
public class Replacefilter {
private NestedString token;
private NestedString value;
private String replaceValue;
private String property;
private StringBuffer inputBuffer;
private StringBuffer outputBuffer = new StringBuffer();
/**
* Validate the filter's configuration.
* @throws BuildException if any part is invalid.
*/
public void validate() throws BuildException {
//Validate mandatory attributes
if (token == null) {
String message = "token is a mandatory for replacefilter.";
throw new BuildException(message);
}
if ("".equals(token.getText())) {
String message = "The token must not be an empty "
+ "string.";
throw new BuildException(message);
}
//value and property are mutually exclusive attributes
if ((value != null) && (property != null)) {
String message = "Either value or property "
+ "can be specified, but a replacefilter "
+ "element cannot have both.";
throw new BuildException(message);
}
if ((property != null)) {
//the property attribute must have access to a property file
if (propertyResource == null) {
String message = "The replacefilter's property attribute "
+ "can only be used with the replacetask's "
+ "propertyFile/Resource attribute.";
throw new BuildException(message);
}
//Make sure property exists in property file
if (properties == null
|| properties.getProperty(property) == null) {
String message = "property \"" + property
+ "\" was not found in " + propertyResource.getName();
throw new BuildException(message);
}
}
replaceValue = getReplaceValue();
}
/**
* Get the replacement value for this filter token.
* @return the replacement value
*/
public String getReplaceValue() {
if (property != null) {
return properties.getProperty(property);
} else if (value != null) {
return value.getText();
} else if (Replace.this.value != null) {
return Replace.this.value.getText();
} else {
//Default is empty string
return "";
}
}
/**
* Set the token to replace.
* @param t String
token.
*/
public void setToken(String t) {
createReplaceToken().addText(t);
}
/**
* Get the string to search for.
* @return current String
token.
*/
public String getToken() {
return token.getText();
}
/**
* The replacement string; required if property
* is not set.
* @param value String
value to replace.
*/
public void setValue(String value) {
createReplaceValue().addText(value);
}
/**
* Get replacement String
.
* @return replacement or null.
*/
public String getValue() {
return value.getText();
}
/**
* Set the name of the property whose value is to serve as
* the replacement value; required if value
is not set.
* @param property property name.
*/
public void setProperty(String property) {
this.property = property;
}
/**
* Get the name of the property whose value is to serve as
* the replacement value.
* @return property or null.
*/
public String getProperty() {
return property;
}
/**
* Create a token to filter as the text of a nested element.
* @return nested token NestedString
to configure.
* @since Ant 1.8.0
*/
public NestedString createReplaceToken() {
if (token == null) {
token = new NestedString();
}
return token;
}
/**
* Create a string to replace the token as the text of a nested element.
* @return replacement value NestedString
to configure.
* @since Ant 1.8.0
*/
public NestedString createReplaceValue() {
if (value == null) {
value = new NestedString();
}
return value;
}
/**
* Retrieves the output buffer of this filter. The filter guarantees
* that data is only appended to the end of this StringBuffer.
* @return The StringBuffer containing the output of this filter.
*/
StringBuffer getOutputBuffer() {
return outputBuffer;
}
/**
* Sets the input buffer for this filter.
* The filter expects from the component providing the input that data
* is only added by that component to the end of this StringBuffer.
* This StringBuffer will be modified by this filter, and expects that
* another component will only apped to this StringBuffer.
* @param input The input for this filter.
*/
void setInputBuffer(StringBuffer input) {
inputBuffer = input;
}
/**
* Processes the buffer as far as possible. Takes into account that
* appended data may make it possible to replace the end of the already
* received data, when the token is split over the "old" and the "new"
* part.
* @return true if some data has been made available in the
* output buffer.
*/
boolean process() {
String t = getToken();
if (inputBuffer.length() > t.length()) {
int pos = replace();
pos = Math.max((inputBuffer.length() - t.length()), pos);
outputBuffer.append(inputBuffer.substring(0, pos));
inputBuffer.delete(0, pos);
return true;
}
return false;
}
/**
* Processes the buffer to the end. Does not take into account that
* appended data may make it possible to replace the end of the already
* received data.
*/
void flush() {
replace();
outputBuffer.append(inputBuffer);
inputBuffer.delete(0, inputBuffer.length());
}
/**
* Performs the replace operation.
* @return The position of the last character that was inserted as
* replacement.
*/
private int replace() {
String t = getToken();
int found = inputBuffer.indexOf(t);
int pos = -1;
final int tokenLength = t.length();
final int replaceValueLength = replaceValue.length();
while (found >= 0) {
inputBuffer.replace(found, found + tokenLength, replaceValue);
pos = found + replaceValueLength;
found = inputBuffer.indexOf(t, pos);
++replaceCount;
}
return pos;
}
}
/**
* Class reading a file in small chunks, and presenting these chunks in
* a StringBuffer. Compatible with the Replacefilter.
* @since 1.7
*/
private class FileInput {
private StringBuffer outputBuffer;
private Reader reader;
private char[] buffer;
private static final int BUFF_SIZE = 4096;
/**
* Constructs the input component. Opens the file for reading.
* @param source The file to read from.
* @throws IOException When the file cannot be read from.
*/
FileInput(File source) throws IOException {
outputBuffer = new StringBuffer();
buffer = new char[BUFF_SIZE];
if (encoding == null) {
reader = new BufferedReader(new FileReader(source));
} else {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(source), encoding));
}
}
/**
* Retrieves the output buffer of this filter. The component guarantees
* that data is only appended to the end of this StringBuffer.
* @return The StringBuffer containing the output of this filter.
*/
StringBuffer getOutputBuffer() {
return outputBuffer;
}
/**
* Reads some data from the file.
* @return true when the end of the file has not been reached.
* @throws IOException When the file cannot be read from.
*/
boolean readChunk() throws IOException {
int bufferLength = 0;
bufferLength = reader.read(buffer);
if (bufferLength < 0) {
return false;
}
outputBuffer.append(new String(buffer, 0, bufferLength));
return true;
}
/**
* Closes the file.
* @throws IOException When the file cannot be closed.
*/
void close() throws IOException {
reader.close();
}
/**
* Closes file but doesn't throw exception
*/
void closeQuietly() {
FileUtils.close(reader);
}
}
/**
* Component writing a file in chunks, taking the chunks from the
* Replacefilter.
* @since 1.7
*/
private class FileOutput {
private StringBuffer inputBuffer;
private Writer writer;
/**
* Constructs the output component. Opens the file for writing.
* @param out The file to read to.
* @throws IOException When the file cannot be read from.
*/
FileOutput(File out) throws IOException {
if (encoding == null) {
writer = new BufferedWriter(new FileWriter(out));
} else {
writer = new BufferedWriter(new OutputStreamWriter
(new FileOutputStream(out), encoding));
}
}
/**
* Sets the input buffer for this component.
* The filter expects from the component providing the input that data
* is only added by that component to the end of this StringBuffer.
* This StringBuffer will be modified by this filter, and expects that
* another component will only append to this StringBuffer.
* @param input The input for this filter.
*/
void setInputBuffer(StringBuffer input) {
inputBuffer = input;
}
/**
* Writes the buffer as far as possible.
* @return false to be inline with the Replacefilter.
* (Yes defining an interface crossed my mind, but would publish the
* internal behavior.)
* @throws IOException when the output cannot be written.
*/
boolean process() throws IOException {
writer.write(inputBuffer.toString());
inputBuffer.delete(0, inputBuffer.length());
return false;
}
/**
* Processes the buffer to the end.
* @throws IOException when the output cannot be flushed.
*/
void flush() throws IOException {
process();
writer.flush();
}
/**
* Closes the file.
* @throws IOException When the file cannot be closed.
*/
void close() throws IOException {
writer.close();
}
/**
* Closes file but doesn't throw exception
*/
void closeQuietly() {
FileUtils.close(writer);
}
}
/**
* Do the execution.
* @throws BuildException if we cant build
*/
public void execute() throws BuildException {
ArrayList savedFilters = (ArrayList) replacefilters.clone();
Properties savedProperties =
properties == null ? null : (Properties) properties.clone();
if (token != null) {
// line separators in values and tokens are "\n"
// in order to compare with the file contents, replace them
// as needed
StringBuffer val = new StringBuffer(value.getText());
stringReplace(val, "\r\n", "\n");
stringReplace(val, "\n", StringUtils.LINE_SEP);
StringBuffer tok = new StringBuffer(token.getText());
stringReplace(tok, "\r\n", "\n");
stringReplace(tok, "\n", StringUtils.LINE_SEP);
Replacefilter firstFilter = createPrimaryfilter();
firstFilter.setToken(tok.toString());
firstFilter.setValue(val.toString());
}
try {
if (replaceFilterResource != null) {
Properties props = getProperties(replaceFilterResource);
Iterator e = props.keySet().iterator();
while (e.hasNext()) {
String tok = e.next().toString();
Replacefilter replaceFilter = createReplacefilter();
replaceFilter.setToken(tok);
replaceFilter.setValue(props.getProperty(tok));
}
}
validateAttributes();
if (propertyResource != null) {
properties = getProperties(propertyResource);
}
validateReplacefilters();
fileCount = 0;
replaceCount = 0;
if (sourceFile != null) {
processFile(sourceFile);
}
if (dir != null) {
DirectoryScanner ds = super.getDirectoryScanner(dir);
String[] srcs = ds.getIncludedFiles();
for (int i = 0; i < srcs.length; i++) {
File file = new File(dir, srcs[i]);
processFile(file);
}
}
if (resources != null) {
for (Iterator i = resources.iterator(); i.hasNext(); ) {
FileProvider fp =
(FileProvider) ((Resource) i.next())
.as(FileProvider.class);
processFile(fp.getFile());
}
}
if (summary) {
log("Replaced " + replaceCount + " occurrences in "
+ fileCount + " files.", Project.MSG_INFO);
}
if (failOnNoReplacements && replaceCount == 0) {
throw new BuildException("didn't replace anything");
}
} finally {
replacefilters = savedFilters;
properties = savedProperties;
} // end of finally
}
/**
* Validate attributes provided for this task in .xml build file.
*
* @exception BuildException if any supplied attribute is invalid or any
* mandatory attribute is missing.
*/
public void validateAttributes() throws BuildException {
if (sourceFile == null && dir == null && resources == null) {
String message = "Either the file or the dir attribute "
+ "or nested resources must be specified";
throw new BuildException(message, getLocation());
}
if (propertyResource != null && !propertyResource.isExists()) {
String message = "Property file " + propertyResource.getName()
+ " does not exist.";
throw new BuildException(message, getLocation());
}
if (token == null && replacefilters.size() == 0) {
String message = "Either token or a nested replacefilter "
+ "must be specified";
throw new BuildException(message, getLocation());
}
if (token != null && "".equals(token.getText())) {
String message = "The token attribute must not be an empty string.";
throw new BuildException(message, getLocation());
}
}
/**
* Validate nested elements.
*
* @exception BuildException if any supplied attribute is invalid or any
* mandatory attribute is missing.
*/
public void validateReplacefilters()
throws BuildException {
final int size = replacefilters.size();
for (int i = 0; i < size; i++) {
Replacefilter element =
(Replacefilter) replacefilters.get(i);
element.validate();
}
}
/**
* Load a properties file.
* @param propertyFile the file to load the properties from.
* @return loaded Properties
object.
* @throws BuildException if the file could not be found or read.
*/
public Properties getProperties(File propertyFile) throws BuildException {
return getProperties(new FileResource(getProject(), propertyFile));
}
/**
* Load a properties resource.
* @param propertyResource the resource to load the properties from.
* @return loaded Properties
object.
* @throws BuildException if the resource could not be found or read.
* @since Ant 1.8.0
*/
public Properties getProperties(Resource propertyResource)
throws BuildException {
Properties props = new Properties();
InputStream in = null;
try {
in = propertyResource.getInputStream();
props.load(in);
} catch (IOException e) {
String message = "Property resource (" + propertyResource.getName()
+ ") cannot be loaded.";
throw new BuildException(message);
} finally {
FileUtils.close(in);
}
return props;
}
/**
* Perform the replacement on the given file.
*
* The replacement is performed on a temporary file which then
* replaces the original file.
*
* @param src the source File
.
*/
private void processFile(File src) throws BuildException {
if (!src.exists()) {
throw new BuildException("Replace: source file " + src.getPath()
+ " doesn't exist", getLocation());
}
File temp = null;
FileInput in = null;
FileOutput out = null;
try {
in = new FileInput(src);
temp = FILE_UTILS.createTempFile("rep", ".tmp",
src.getParentFile(), false, true);
out = new FileOutput(temp);
int repCountStart = replaceCount;
logFilterChain(src.getPath());
out.setInputBuffer(buildFilterChain(in.getOutputBuffer()));
while (in.readChunk()) {
if (processFilterChain()) {
out.process();
}
}
flushFilterChain();
out.flush();
in.close();
in = null;
out.close();
out = null;
boolean changes = (replaceCount != repCountStart);
if (changes) {
fileCount++;
long origLastModified = src.lastModified();
FILE_UTILS.rename(temp, src);
if (preserveLastModified) {
FILE_UTILS.setFileLastModified(src, origLastModified);
}
temp = null;
}
} catch (IOException ioe) {
throw new BuildException("IOException in " + src + " - "
+ ioe.getClass().getName() + ":"
+ ioe.getMessage(), ioe, getLocation());
} finally {
if (null != in) {
in.closeQuietly();
}
if (null != out) {
out.closeQuietly();
}
if (temp != null) {
if (!temp.delete()) {
temp.deleteOnExit();
}
}
}
}
/**
* Flushes all filters.
*/
private void flushFilterChain() {
final int size = replacefilters.size();
for (int i = 0; i < size; i++) {
Replacefilter filter = (Replacefilter) replacefilters.get(i);
filter.flush();
}
}
/**
* Performs the normal processing of the filters.
* @return true if the filter chain produced new output.
*/
private boolean processFilterChain() {
final int size = replacefilters.size();
for (int i = 0; i < size; i++) {
Replacefilter filter = (Replacefilter) replacefilters.get(i);
if (!filter.process()) {
return false;
}
}
return true;
}
/**
* Creates the chain of filters to operate.
* @param inputBuffer StringBuffer
containing the input for the
* first filter.
* @return StringBuffer
containing the output of the last filter.
*/
private StringBuffer buildFilterChain(StringBuffer inputBuffer) {
StringBuffer buf = inputBuffer;
final int size = replacefilters.size();
for (int i = 0; i < size; i++) {
Replacefilter filter = (Replacefilter) replacefilters.get(i);
filter.setInputBuffer(buf);
buf = filter.getOutputBuffer();
}
return buf;
}
/**
* Logs the chain of filters to operate on the file.
* @param filename String
.
*/
private void logFilterChain(String filename) {
final int size = replacefilters.size();
for (int i = 0; i < size; i++) {
Replacefilter filter = (Replacefilter) replacefilters.get(i);
log("Replacing in " + filename + ": " + filter.getToken()
+ " --> " + filter.getReplaceValue(), Project.MSG_VERBOSE);
}
}
/**
* Set the source file; required unless dir
is set.
* @param file source File
.
*/
public void setFile(File file) {
this.sourceFile = file;
}
/**
* Indicates whether a summary of the replace operation should be
* produced, detailing how many token occurrences and files were
* processed; optional, default=false
.
*
* @param summary boolean
whether a summary of the
* replace operation should be logged.
*/
public void setSummary(boolean summary) {
this.summary = summary;
}
/**
* Sets the name of a property file containing filters; optional.
* Each property will be treated as a replacefilter where token is the name
* of the property and value is the value of the property.
* @param replaceFilterFile File
to load.
*/
public void setReplaceFilterFile(File replaceFilterFile) {
setReplaceFilterResource(new FileResource(getProject(),
replaceFilterFile));
}
/**
* Sets the name of a resource containing filters; optional.
* Each property will be treated as a replacefilter where token is the name
* of the property and value is the value of the property.
* @param replaceFilter Resource
to load.
* @since Ant 1.8.0
*/
public void setReplaceFilterResource(Resource replaceFilter) {
this.replaceFilterResource = replaceFilter;
}
/**
* The base directory to use when replacing a token in multiple files;
* required if file
is not defined.
* @param dir File
representing the base directory.
*/
public void setDir(File dir) {
this.dir = dir;
}
/**
* Set the string token to replace; required unless a nested
* replacetoken
element or the
* replacefilterresource
attribute is used.
* @param token token String
.
*/
public void setToken(String token) {
createReplaceToken().addText(token);
}
/**
* Set the string value to use as token replacement;
* optional, default is the empty string "".
* @param value replacement value.
*/
public void setValue(String value) {
createReplaceValue().addText(value);
}
/**
* Set the file encoding to use on the files read and written by the task;
* optional, defaults to default JVM encoding.
*
* @param encoding the encoding to use on the files.
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Create a token to filter as the text of a nested element.
* @return nested token NestedString
to configure.
*/
public NestedString createReplaceToken() {
if (token == null) {
token = new NestedString();
}
return token;
}
/**
* Create a string to replace the token as the text of a nested element.
* @return replacement value NestedString
to configure.
*/
public NestedString createReplaceValue() {
return value;
}
/**
* The name of a property file from which properties specified using nested
* <replacefilter>
elements are drawn; required only if
* the property attribute of <replacefilter>
is used.
* @param propertyFile File
to load.
*/
public void setPropertyFile(File propertyFile) {
setPropertyResource(new FileResource(propertyFile));
}
/**
* A resource from which properties specified using nested
* <replacefilter>
elements are drawn; required
* only if the property attribute of
* <replacefilter>
is used.
* @param propertyResource Resource
to load.
*
* @since Ant 1.8.0
*/
public void setPropertyResource(Resource propertyResource) {
this.propertyResource = propertyResource;
}
/**
* Add a nested <replacefilter> element.
* @return a nested Replacefilter
object to be configured.
*/
public Replacefilter createReplacefilter() {
Replacefilter filter = new Replacefilter();
replacefilters.add(filter);
return filter;
}
/**
* 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);
}
/**
* 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;
}
/**
* Whether the build should fail if nothing has been replaced.
*
* @since Ant 1.8.0
*/
public void setFailOnNoReplacements(boolean b) {
failOnNoReplacements = b;
}
/**
* Adds the token and value as first <replacefilter> element.
* The token and value are always processed first.
* @return a nested Replacefilter
object to be configured.
*/
private Replacefilter createPrimaryfilter() {
Replacefilter filter = new Replacefilter();
replacefilters.add(0, filter);
return filter;
}
/**
* Replace occurrences of str1 in StringBuffer str with str2.
*/
private void stringReplace(StringBuffer str, String str1, String str2) {
int found = str.indexOf(str1);
final int str1Length = str1.length();
final int str2Length = str2.length();
while (found >= 0) {
str.replace(found, found + str1Length, str2);
found = str.indexOf(str1, found + str2Length);
}
}
}