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:
019: package org.apache.tools.ant.taskdefs;
020:
021: import java.io.BufferedReader;
022: import java.io.BufferedWriter;
023: import java.io.File;
024: import java.io.FileInputStream;
025: import java.io.FileNotFoundException;
026: import java.io.FileOutputStream;
027: import java.io.FileReader;
028: import java.io.FileWriter;
029: import java.io.IOException;
030: import java.io.InputStreamReader;
031: import java.io.OutputStreamWriter;
032: import java.io.Reader;
033: import java.io.Writer;
034: import java.util.Enumeration;
035: import java.util.Properties;
036: import java.util.Vector;
037: import org.apache.tools.ant.BuildException;
038: import org.apache.tools.ant.DirectoryScanner;
039: import org.apache.tools.ant.Project;
040: import org.apache.tools.ant.util.FileUtils;
041: import org.apache.tools.ant.util.StringUtils;
042:
043: /**
044: * Replaces all occurrences of one or more string tokens with given
045: * values in the indicated files. Each value can be either a string
046: * or the value of a property available in a designated property file.
047: * If you want to replace a text that crosses line boundaries, you
048: * must use a nested <code><replacetoken></code> element.
049: *
050: * @since Ant 1.1
051: *
052: * @ant.task category="filesystem"
053: */
054: public class Replace extends MatchingTask {
055:
056: private static final FileUtils FILE_UTILS = FileUtils
057: .getFileUtils();
058:
059: private File src = null;
060: private NestedString token = null;
061: private NestedString value = new NestedString();
062:
063: private File propertyFile = null;
064: private File replaceFilterFile = null;
065: private Properties properties = null;
066: private Vector replacefilters = new Vector();
067:
068: private File dir = null;
069:
070: private int fileCount;
071: private int replaceCount;
072: private boolean summary = false;
073:
074: /** The encoding used to read and write files - if null, uses default */
075: private String encoding = null;
076:
077: /**
078: * An inline string to use as the replacement text.
079: */
080: public class NestedString {
081:
082: private StringBuffer buf = new StringBuffer();
083:
084: /**
085: * The text of the element.
086: *
087: * @param val the string to add
088: */
089: public void addText(String val) {
090: buf.append(val);
091: }
092:
093: /**
094: * @return the text
095: */
096: public String getText() {
097: return buf.toString();
098: }
099: }
100:
101: /**
102: * A filter to apply.
103: */
104: public class Replacefilter {
105: private String token;
106: private String value;
107: private String replaceValue;
108: private String property;
109:
110: private StringBuffer inputBuffer;
111: private StringBuffer outputBuffer = new StringBuffer();
112:
113: /**
114: * Validate the filter's configuration.
115: * @throws BuildException if any part is invalid.
116: */
117: public void validate() throws BuildException {
118: //Validate mandatory attributes
119: if (token == null) {
120: String message = "token is a mandatory attribute "
121: + "of replacefilter.";
122: throw new BuildException(message);
123: }
124:
125: if ("".equals(token)) {
126: String message = "The token attribute must not be an empty "
127: + "string.";
128: throw new BuildException(message);
129: }
130:
131: //value and property are mutually exclusive attributes
132: if ((value != null) && (property != null)) {
133: String message = "Either value or property "
134: + "can be specified, but a replacefilter "
135: + "element cannot have both.";
136: throw new BuildException(message);
137: }
138:
139: if ((property != null)) {
140: //the property attribute must have access to a property file
141: if (propertyFile == null) {
142: String message = "The replacefilter's property attribute "
143: + "can only be used with the replacetask's "
144: + "propertyFile attribute.";
145: throw new BuildException(message);
146: }
147:
148: //Make sure property exists in property file
149: if (properties == null
150: || properties.getProperty(property) == null) {
151: String message = "property \"" + property
152: + "\" was not found in "
153: + propertyFile.getPath();
154: throw new BuildException(message);
155: }
156: }
157:
158: replaceValue = getReplaceValue();
159: }
160:
161: /**
162: * Get the replacement value for this filter token.
163: * @return the replacement value
164: */
165: public String getReplaceValue() {
166: if (property != null) {
167: return properties.getProperty(property);
168: } else if (value != null) {
169: return value;
170: } else if (Replace.this .value != null) {
171: return Replace.this .value.getText();
172: } else {
173: //Default is empty string
174: return "";
175: }
176: }
177:
178: /**
179: * Set the token to replace.
180: * @param token <code>String</code> token.
181: */
182: public void setToken(String token) {
183: this .token = token;
184: }
185:
186: /**
187: * Get the string to search for.
188: * @return current <code>String</code> token.
189: */
190: public String getToken() {
191: return token;
192: }
193:
194: /**
195: * The replacement string; required if <code>property<code>
196: * is not set.
197: * @param value <code>String</code> value to replace.
198: */
199: public void setValue(String value) {
200: this .value = value;
201: }
202:
203: /**
204: * Get replacement <code>String</code>.
205: * @return replacement or null.
206: */
207: public String getValue() {
208: return value;
209: }
210:
211: /**
212: * Set the name of the property whose value is to serve as
213: * the replacement value; required if <code>value</code> is not set.
214: * @param property property name.
215: */
216: public void setProperty(String property) {
217: this .property = property;
218: }
219:
220: /**
221: * Get the name of the property whose value is to serve as
222: * the replacement value.
223: * @return property or null.
224: */
225: public String getProperty() {
226: return property;
227: }
228:
229: /**
230: * Retrieves the output buffer of this filter. The filter guarantees
231: * that data is only appended to the end of this StringBuffer.
232: * @return The StringBuffer containing the output of this filter.
233: */
234: StringBuffer getOutputBuffer() {
235: return outputBuffer;
236: }
237:
238: /**
239: * Sets the input buffer for this filter.
240: * The filter expects from the component providing the input that data
241: * is only added by that component to the end of this StringBuffer.
242: * This StringBuffer will be modified by this filter, and expects that
243: * another component will only apped to this StringBuffer.
244: * @param input The input for this filter.
245: */
246: void setInputBuffer(StringBuffer input) {
247: inputBuffer = input;
248: }
249:
250: /**
251: * Processes the buffer as far as possible. Takes into account that
252: * appended data may make it possible to replace the end of the already
253: * received data, when the token is split over the "old" and the "new"
254: * part.
255: * @return true if some data has been made available in the
256: * output buffer.
257: */
258: boolean process() {
259: if (inputBuffer.length() > token.length()) {
260: int pos = replace();
261: pos = Math.max((inputBuffer.length() - token.length()),
262: pos);
263: outputBuffer.append(inputBuffer.substring(0, pos));
264: inputBuffer.delete(0, pos);
265: return true;
266: }
267: return false;
268: }
269:
270: /**
271: * Processes the buffer to the end. Does not take into account that
272: * appended data may make it possible to replace the end of the already
273: * received data.
274: */
275: void flush() {
276: replace();
277: // Avoid runtime problem on pre 1.4 when compiling post 1.4
278: outputBuffer.append(inputBuffer.toString());
279: inputBuffer.delete(0, inputBuffer.length());
280: }
281:
282: /**
283: * Performs the replace operation.
284: * @return The position of the last character that was inserted as
285: * replacement.
286: */
287: private int replace() {
288: int found = inputBuffer.toString().indexOf(token);
289: int pos = -1;
290: while (found >= 0) {
291: inputBuffer.replace(found, found + token.length(),
292: replaceValue);
293: pos = found + replaceValue.length();
294: found = inputBuffer.toString().indexOf(token, pos);
295: ++replaceCount;
296: }
297: return pos;
298: }
299: }
300:
301: /**
302: * Class reading a file in small chunks, and presenting these chunks in
303: * a StringBuffer. Compatible with the Replacefilter.
304: * @since 1.7
305: */
306: private class FileInput {
307: private StringBuffer outputBuffer;
308: private Reader reader;
309: private char[] buffer;
310: private static final int BUFF_SIZE = 4096;
311:
312: /**
313: * Constructs the input component. Opens the file for reading.
314: * @param source The file to read from.
315: * @throws IOException When the file cannot be read from.
316: */
317: FileInput(File source) throws IOException {
318: outputBuffer = new StringBuffer();
319: buffer = new char[BUFF_SIZE];
320: if (encoding == null) {
321: reader = new BufferedReader(new FileReader(source));
322: } else {
323: reader = new BufferedReader(new InputStreamReader(
324: new FileInputStream(source), encoding));
325: }
326: }
327:
328: /**
329: * Retrieves the output buffer of this filter. The component guarantees
330: * that data is only appended to the end of this StringBuffer.
331: * @return The StringBuffer containing the output of this filter.
332: */
333: StringBuffer getOutputBuffer() {
334: return outputBuffer;
335: }
336:
337: /**
338: * Reads some data from the file.
339: * @return true when the end of the file has not been reached.
340: * @throws IOException When the file cannot be read from.
341: */
342: boolean readChunk() throws IOException {
343: int bufferLength = 0;
344: bufferLength = reader.read(buffer);
345: if (bufferLength < 0) {
346: return false;
347: }
348: outputBuffer.append(new String(buffer, 0, bufferLength));
349: return true;
350: }
351:
352: /**
353: * Closes the file.
354: * @throws IOException When the file cannot be closed.
355: */
356: void close() throws IOException {
357: reader.close();
358: }
359:
360: /**
361: * Closes file but doesn't throw exception
362: */
363: void closeQuietly() {
364: FileUtils.close(reader);
365: }
366:
367: }
368:
369: /**
370: * Component writing a file in chunks, taking the chunks from the
371: * Replacefilter.
372: * @since 1.7
373: */
374: private class FileOutput {
375: private StringBuffer inputBuffer;
376: private Writer writer;
377:
378: /**
379: * Constructs the output component. Opens the file for writing.
380: * @param out The file to read to.
381: * @throws IOException When the file cannot be read from.
382: */
383: FileOutput(File out) throws IOException {
384: if (encoding == null) {
385: writer = new BufferedWriter(new FileWriter(out));
386: } else {
387: writer = new BufferedWriter(new OutputStreamWriter(
388: new FileOutputStream(out), encoding));
389: }
390: }
391:
392: /**
393: * Sets the input buffer for this component.
394: * The filter expects from the component providing the input that data
395: * is only added by that component to the end of this StringBuffer.
396: * This StringBuffer will be modified by this filter, and expects that
397: * another component will only append to this StringBuffer.
398: * @param input The input for this filter.
399: */
400: void setInputBuffer(StringBuffer input) {
401: inputBuffer = input;
402: }
403:
404: /**
405: * Writes the buffer as far as possible.
406: * @return false to be inline with the Replacefilter.
407: * (Yes defining an interface crossed my mind, but would publish the
408: * internal behavior.)
409: * @throws IOException when the output cannot be written.
410: */
411: boolean process() throws IOException {
412: writer.write(inputBuffer.toString());
413: inputBuffer.delete(0, inputBuffer.length());
414: return false;
415: }
416:
417: /**
418: * Processes the buffer to the end.
419: * @throws IOException when the output cannot be flushed.
420: */
421: void flush() throws IOException {
422: process();
423: writer.flush();
424: }
425:
426: /**
427: * Closes the file.
428: * @throws IOException When the file cannot be closed.
429: */
430: void close() throws IOException {
431: writer.close();
432: }
433:
434: /**
435: * Closes file but doesn't throw exception
436: */
437: void closeQuietly() {
438: FileUtils.close(writer);
439: }
440: }
441:
442: /**
443: * Do the execution.
444: * @throws BuildException if we cant build
445: */
446: public void execute() throws BuildException {
447:
448: Vector savedFilters = (Vector) replacefilters.clone();
449: Properties savedProperties = properties == null ? null
450: : (Properties) properties.clone();
451:
452: if (token != null) {
453: // line separators in values and tokens are "\n"
454: // in order to compare with the file contents, replace them
455: // as needed
456: StringBuffer val = new StringBuffer(value.getText());
457: stringReplace(val, "\r\n", "\n");
458: stringReplace(val, "\n", StringUtils.LINE_SEP);
459: StringBuffer tok = new StringBuffer(token.getText());
460: stringReplace(tok, "\r\n", "\n");
461: stringReplace(tok, "\n", StringUtils.LINE_SEP);
462: Replacefilter firstFilter = createPrimaryfilter();
463: firstFilter.setToken(tok.toString());
464: firstFilter.setValue(val.toString());
465: }
466:
467: try {
468: if (replaceFilterFile != null) {
469: Properties props = getProperties(replaceFilterFile);
470: Enumeration e = props.keys();
471: while (e.hasMoreElements()) {
472: String tok = e.nextElement().toString();
473: Replacefilter replaceFilter = createReplacefilter();
474: replaceFilter.setToken(tok);
475: replaceFilter.setValue(props.getProperty(tok));
476: }
477: }
478:
479: validateAttributes();
480:
481: if (propertyFile != null) {
482: properties = getProperties(propertyFile);
483: }
484:
485: validateReplacefilters();
486: fileCount = 0;
487: replaceCount = 0;
488:
489: if (src != null) {
490: processFile(src);
491: }
492:
493: if (dir != null) {
494: DirectoryScanner ds = super .getDirectoryScanner(dir);
495: String[] srcs = ds.getIncludedFiles();
496:
497: for (int i = 0; i < srcs.length; i++) {
498: File file = new File(dir, srcs[i]);
499: processFile(file);
500: }
501: }
502:
503: if (summary) {
504: log("Replaced " + replaceCount + " occurrences in "
505: + fileCount + " files.", Project.MSG_INFO);
506: }
507: } finally {
508: replacefilters = savedFilters;
509: properties = savedProperties;
510: } // end of finally
511:
512: }
513:
514: /**
515: * Validate attributes provided for this task in .xml build file.
516: *
517: * @exception BuildException if any supplied attribute is invalid or any
518: * mandatory attribute is missing.
519: */
520: public void validateAttributes() throws BuildException {
521: if (src == null && dir == null) {
522: String message = "Either the file or the dir attribute "
523: + "must be specified";
524: throw new BuildException(message, getLocation());
525: }
526: if (propertyFile != null && !propertyFile.exists()) {
527: String message = "Property file " + propertyFile.getPath()
528: + " does not exist.";
529: throw new BuildException(message, getLocation());
530: }
531: if (token == null && replacefilters.size() == 0) {
532: String message = "Either token or a nested replacefilter "
533: + "must be specified";
534: throw new BuildException(message, getLocation());
535: }
536: if (token != null && "".equals(token.getText())) {
537: String message = "The token attribute must not be an empty string.";
538: throw new BuildException(message, getLocation());
539: }
540: }
541:
542: /**
543: * Validate nested elements.
544: *
545: * @exception BuildException if any supplied attribute is invalid or any
546: * mandatory attribute is missing.
547: */
548: public void validateReplacefilters() throws BuildException {
549: for (int i = 0; i < replacefilters.size(); i++) {
550: Replacefilter element = (Replacefilter) replacefilters
551: .elementAt(i);
552: element.validate();
553: }
554: }
555:
556: /**
557: * Load a properties file.
558: * @param propertyFile the file to load the properties from.
559: * @return loaded <code>Properties</code> object.
560: * @throws BuildException if the file could not be found or read.
561: */
562: public Properties getProperties(File propertyFile)
563: throws BuildException {
564: Properties props = new Properties();
565:
566: FileInputStream in = null;
567: try {
568: in = new FileInputStream(propertyFile);
569: props.load(in);
570: } catch (FileNotFoundException e) {
571: String message = "Property file (" + propertyFile.getPath()
572: + ") not found.";
573: throw new BuildException(message);
574: } catch (IOException e) {
575: String message = "Property file (" + propertyFile.getPath()
576: + ") cannot be loaded.";
577: throw new BuildException(message);
578: } finally {
579: FileUtils.close(in);
580: }
581:
582: return props;
583: }
584:
585: /**
586: * Perform the replacement on the given file.
587: *
588: * The replacement is performed on a temporary file which then
589: * replaces the original file.
590: *
591: * @param src the source <code>File</code>.
592: */
593: private void processFile(File src) throws BuildException {
594: if (!src.exists()) {
595: throw new BuildException("Replace: source file "
596: + src.getPath() + " doesn't exist", getLocation());
597: }
598:
599: File temp = null;
600: FileInput in = null;
601: FileOutput out = null;
602: try {
603: in = new FileInput(src);
604:
605: temp = FILE_UTILS.createTempFile("rep", ".tmp", src
606: .getParentFile());
607: out = new FileOutput(temp);
608:
609: int repCountStart = replaceCount;
610:
611: logFilterChain(src.getPath());
612:
613: out.setInputBuffer(buildFilterChain(in.getOutputBuffer()));
614:
615: while (in.readChunk()) {
616: if (processFilterChain()) {
617: out.process();
618: }
619: }
620:
621: flushFilterChain();
622:
623: out.flush();
624: in.close();
625: in = null;
626: out.close();
627: out = null;
628:
629: boolean changes = (replaceCount != repCountStart);
630: if (changes) {
631: FILE_UTILS.rename(temp, src);
632: temp = null;
633: }
634: } catch (IOException ioe) {
635: throw new BuildException(
636: "IOException in " + src + " - "
637: + ioe.getClass().getName() + ":"
638: + ioe.getMessage(), ioe, getLocation());
639: } finally {
640: if (null != in) {
641: in.closeQuietly();
642: }
643: if (null != out) {
644: out.closeQuietly();
645: }
646: if (temp != null) {
647: if (!temp.delete()) {
648: temp.deleteOnExit();
649: }
650: }
651: }
652: }
653:
654: /**
655: * Flushes all filters.
656: */
657: private void flushFilterChain() {
658: for (int i = 0; i < replacefilters.size(); i++) {
659: Replacefilter filter = (Replacefilter) replacefilters
660: .elementAt(i);
661: filter.flush();
662: }
663: }
664:
665: /**
666: * Performs the normal processing of the filters.
667: * @return true if the filter chain produced new output.
668: */
669: private boolean processFilterChain() {
670: for (int i = 0; i < replacefilters.size(); i++) {
671: Replacefilter filter = (Replacefilter) replacefilters
672: .elementAt(i);
673: if (!filter.process()) {
674: return false;
675: }
676: }
677: return true;
678: }
679:
680: /**
681: * Creates the chain of filters to operate.
682: * @param inputBuffer <code>StringBuffer</code> containing the input for the
683: * first filter.
684: * @return <code>StringBuffer</code> containing the output of the last filter.
685: */
686: private StringBuffer buildFilterChain(StringBuffer inputBuffer) {
687: StringBuffer buf = inputBuffer;
688: for (int i = 0; i < replacefilters.size(); i++) {
689: Replacefilter filter = (Replacefilter) replacefilters
690: .elementAt(i);
691: filter.setInputBuffer(buf);
692: buf = filter.getOutputBuffer();
693: }
694: return buf;
695: }
696:
697: /**
698: * Logs the chain of filters to operate on the file.
699: * @param filename <code>String</code>.
700: */
701: private void logFilterChain(String filename) {
702: for (int i = 0; i < replacefilters.size(); i++) {
703: Replacefilter filter = (Replacefilter) replacefilters
704: .elementAt(i);
705: log("Replacing in " + filename + ": " + filter.getToken()
706: + " --> " + filter.getReplaceValue(),
707: Project.MSG_VERBOSE);
708: }
709: }
710:
711: /**
712: * Set the source file; required unless <code>dir</code> is set.
713: * @param file source <code>File</code>.
714: */
715: public void setFile(File file) {
716: this .src = file;
717: }
718:
719: /**
720: * Indicates whether a summary of the replace operation should be
721: * produced, detailing how many token occurrences and files were
722: * processed; optional, default=<code>false</code>.
723: *
724: * @param summary <code>boolean</code> whether a summary of the
725: * replace operation should be logged.
726: */
727: public void setSummary(boolean summary) {
728: this .summary = summary;
729: }
730:
731: /**
732: * Sets the name of a property file containing filters; optional.
733: * Each property will be treated as a replacefilter where token is the name
734: * of the property and value is the value of the property.
735: * @param replaceFilterFile <code>File</code> to load.
736: */
737: public void setReplaceFilterFile(File replaceFilterFile) {
738: this .replaceFilterFile = replaceFilterFile;
739: }
740:
741: /**
742: * The base directory to use when replacing a token in multiple files;
743: * required if <code>file</code> is not defined.
744: * @param dir <code>File</code> representing the base directory.
745: */
746: public void setDir(File dir) {
747: this .dir = dir;
748: }
749:
750: /**
751: * Set the string token to replace; required unless a nested
752: * <code>replacetoken</code> element or the <code>replacefilterfile</code>
753: * attribute is used.
754: * @param token token <code>String</code>.
755: */
756: public void setToken(String token) {
757: createReplaceToken().addText(token);
758: }
759:
760: /**
761: * Set the string value to use as token replacement;
762: * optional, default is the empty string "".
763: * @param value replacement value.
764: */
765: public void setValue(String value) {
766: createReplaceValue().addText(value);
767: }
768:
769: /**
770: * Set the file encoding to use on the files read and written by the task;
771: * optional, defaults to default JVM encoding.
772: *
773: * @param encoding the encoding to use on the files.
774: */
775: public void setEncoding(String encoding) {
776: this .encoding = encoding;
777: }
778:
779: /**
780: * Create a token to filter as the text of a nested element.
781: * @return nested token <code>NestedString</code> to configure.
782: */
783: public NestedString createReplaceToken() {
784: if (token == null) {
785: token = new NestedString();
786: }
787: return token;
788: }
789:
790: /**
791: * Create a string to replace the token as the text of a nested element.
792: * @return replacement value <code>NestedString</code> to configure.
793: */
794: public NestedString createReplaceValue() {
795: return value;
796: }
797:
798: /**
799: * The name of a property file from which properties specified using nested
800: * <code><replacefilter></code> elements are drawn; required only if
801: * the <i>property</i> attribute of <code><replacefilter></code> is used.
802: * @param propertyFile <code>File</code> to load.
803: */
804: public void setPropertyFile(File propertyFile) {
805: this .propertyFile = propertyFile;
806: }
807:
808: /**
809: * Add a nested <replacefilter> element.
810: * @return a nested <code>Replacefilter</code> object to be configured.
811: */
812: public Replacefilter createReplacefilter() {
813: Replacefilter filter = new Replacefilter();
814: replacefilters.addElement(filter);
815: return filter;
816: }
817:
818: /**
819: * Adds the token and value as first <replacefilter> element.
820: * The token and value are always processed first.
821: * @return a nested <code>Replacefilter</code> object to be configured.
822: */
823: private Replacefilter createPrimaryfilter() {
824: Replacefilter filter = new Replacefilter();
825: replacefilters.insertElementAt(filter, 0);
826: return filter;
827: }
828:
829: /**
830: * Replace occurrences of str1 in StringBuffer str with str2.
831: */
832: private void stringReplace(StringBuffer str, String str1,
833: String str2) {
834: int found = str.toString().indexOf(str1);
835: while (found >= 0) {
836: str.replace(found, found + str1.length(), str2);
837: found = str.toString().indexOf(str1, found + str2.length());
838: }
839: }
840:
841: }
|