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: package org.apache.tools.ant.types;
019:
020: import java.io.File;
021: import java.io.FileInputStream;
022: import java.util.Enumeration;
023: import java.util.Hashtable;
024: import java.util.Properties;
025: import java.util.Vector;
026:
027: import org.apache.tools.ant.Project;
028: import org.apache.tools.ant.BuildException;
029: import org.apache.tools.ant.util.FileUtils;
030:
031: /**
032: * A set of filters to be applied to something.
033: *
034: * A filter set may have begintoken and endtokens defined.
035: *
036: */
037: public class FilterSet extends DataType implements Cloneable {
038:
039: /**
040: * Individual filter component of filterset.
041: *
042: */
043: public static class Filter {
044: // CheckStyle:VisibilityModifier OFF - bc
045: /** Token which will be replaced in the filter operation. */
046: String token;
047:
048: /** The value which will replace the token in the filtering operation. */
049: String value;
050:
051: // CheckStyle:VisibilityModifier ON
052:
053: /**
054: * Constructor for the Filter object.
055: *
056: * @param token The token which will be replaced when filtering.
057: * @param value The value which will replace the token when filtering.
058: */
059: public Filter(String token, String value) {
060: setToken(token);
061: setValue(value);
062: }
063:
064: /**
065: * No-argument conmstructor.
066: */
067: public Filter() {
068: }
069:
070: /**
071: * Sets the Token attribute of the Filter object.
072: *
073: * @param token The new Token value.
074: */
075: public void setToken(String token) {
076: this .token = token;
077: }
078:
079: /**
080: * Sets the Value attribute of the Filter object.
081: *
082: * @param value The new Value value.
083: */
084: public void setValue(String value) {
085: this .value = value;
086: }
087:
088: /**
089: * Gets the Token attribute of the Filter object.
090: *
091: * @return The Token value.
092: */
093: public String getToken() {
094: return token;
095: }
096:
097: /**
098: * Gets the Value attribute of the Filter object.
099: *
100: * @return The Value value.
101: */
102: public String getValue() {
103: return value;
104: }
105: }
106:
107: /**
108: * The filtersfile nested element.
109: *
110: */
111: public class FiltersFile {
112:
113: /**
114: * Constructor for the FiltersFile object.
115: */
116: public FiltersFile() {
117: }
118:
119: /**
120: * Sets the file from which filters will be read.
121: *
122: * @param file the file from which filters will be read.
123: */
124: public void setFile(File file) {
125: filtersFiles.add(file);
126: }
127: }
128:
129: /**
130: * EnumeratedAttribute to set behavior WRT missing filtersfiles:
131: * "fail" (default), "warn", "ignore".
132: * @since Ant 1.7
133: */
134: public static class OnMissing extends EnumeratedAttribute {
135: private static final String[] VALUES = new String[] { "fail",
136: "warn", "ignore" };
137:
138: /** Fail value */
139: public static final OnMissing FAIL = new OnMissing("fail");
140: /** Warn value */
141: public static final OnMissing WARN = new OnMissing("warn");
142: /** Ignore value */
143: public static final OnMissing IGNORE = new OnMissing("ignore");
144:
145: private static final int FAIL_INDEX = 0;
146: private static final int WARN_INDEX = 1;
147: private static final int IGNORE_INDEX = 2;
148:
149: /**
150: * Default constructor.
151: */
152: public OnMissing() {
153: }
154:
155: /**
156: * Convenience constructor.
157: * @param value the value to set.
158: */
159: public OnMissing(String value) {
160: setValue(value);
161: }
162:
163: //inherit doc
164: /** {@inheritDoc}. */
165: public String[] getValues() {
166: return VALUES;
167: }
168: }
169:
170: /** The default token start string */
171: public static final String DEFAULT_TOKEN_START = "@";
172:
173: /** The default token end string */
174: public static final String DEFAULT_TOKEN_END = "@";
175:
176: private String startOfToken = DEFAULT_TOKEN_START;
177: private String endOfToken = DEFAULT_TOKEN_END;
178:
179: /** Contains a list of parsed tokens */
180: private Vector passedTokens;
181: /** if a duplicate token is found, this is set to true */
182: private boolean duplicateToken = false;
183:
184: private boolean recurse = true;
185: private Hashtable filterHash = null;
186: private Vector filtersFiles = new Vector();
187: private OnMissing onMissingFiltersFile = OnMissing.FAIL;
188: private boolean readingFiles = false;
189:
190: private int recurseDepth = 0;
191:
192: /**
193: * List of ordered filters and filter files.
194: */
195: private Vector filters = new Vector();
196:
197: /**
198: * Default constructor.
199: */
200: public FilterSet() {
201: }
202:
203: /**
204: * Create a Filterset from another filterset.
205: *
206: * @param filterset the filterset upon which this filterset will be based.
207: */
208: protected FilterSet(FilterSet filterset) {
209: super ();
210: this .filters = (Vector) filterset.getFilters().clone();
211: }
212:
213: /**
214: * Get the filters in the filter set.
215: *
216: * @return a Vector of Filter instances.
217: */
218: protected synchronized Vector getFilters() {
219: if (isReference()) {
220: return getRef().getFilters();
221: }
222: //silly hack to avoid stack overflow...
223: if (!readingFiles) {
224: readingFiles = true;
225: for (int i = 0, sz = filtersFiles.size(); i < sz; i++) {
226: readFiltersFromFile((File) filtersFiles.get(i));
227: }
228: filtersFiles.clear();
229: readingFiles = false;
230: }
231: return filters;
232: }
233:
234: /**
235: * Get the referenced filter set.
236: *
237: * @return the filterset from the reference.
238: */
239: protected FilterSet getRef() {
240: return (FilterSet) getCheckedRef(FilterSet.class, "filterset");
241: }
242:
243: /**
244: * Gets the filter hash of the FilterSet.
245: *
246: * @return The hash of the tokens and values for quick lookup.
247: */
248: public synchronized Hashtable getFilterHash() {
249: if (filterHash == null) {
250: filterHash = new Hashtable(getFilters().size());
251: for (Enumeration e = getFilters().elements(); e
252: .hasMoreElements();) {
253: Filter filter = (Filter) e.nextElement();
254: filterHash.put(filter.getToken(), filter.getValue());
255: }
256: }
257: return filterHash;
258: }
259:
260: /**
261: * Set the file containing the filters for this filterset.
262: *
263: * @param filtersFile sets the filter file from which to read filters
264: * for this filter set.
265: * @throws BuildException if there is an error.
266: */
267: public void setFiltersfile(File filtersFile) throws BuildException {
268: if (isReference()) {
269: throw tooManyAttributes();
270: }
271: filtersFiles.add(filtersFile);
272: }
273:
274: /**
275: * Set the string used to id the beginning of a token.
276: *
277: * @param startOfToken The new Begintoken value.
278: */
279: public void setBeginToken(String startOfToken) {
280: if (isReference()) {
281: throw tooManyAttributes();
282: }
283: if (startOfToken == null || "".equals(startOfToken)) {
284: throw new BuildException("beginToken must not be empty");
285: }
286: this .startOfToken = startOfToken;
287: }
288:
289: /**
290: * Get the begin token for this filterset.
291: *
292: * @return the filter set's begin token for filtering.
293: */
294: public String getBeginToken() {
295: if (isReference()) {
296: return getRef().getBeginToken();
297: }
298: return startOfToken;
299: }
300:
301: /**
302: * Set the string used to id the end of a token.
303: *
304: * @param endOfToken The new Endtoken value.
305: */
306: public void setEndToken(String endOfToken) {
307: if (isReference()) {
308: throw tooManyAttributes();
309: }
310: if (endOfToken == null || "".equals(endOfToken)) {
311: throw new BuildException("endToken must not be empty");
312: }
313: this .endOfToken = endOfToken;
314: }
315:
316: /**
317: * Get the end token for this filterset.
318: *
319: * @return the filter set's end token for replacement delimiting.
320: */
321: public String getEndToken() {
322: if (isReference()) {
323: return getRef().getEndToken();
324: }
325: return endOfToken;
326: }
327:
328: /**
329: * Set whether recursive token expansion is enabled.
330: * @param recurse <code>boolean</code> whether to recurse.
331: */
332: public void setRecurse(boolean recurse) {
333: this .recurse = recurse;
334: }
335:
336: /**
337: * Get whether recursive token expansion is enabled.
338: * @return <code>boolean</code> whether enabled.
339: */
340: public boolean isRecurse() {
341: return recurse;
342: }
343:
344: /**
345: * Read the filters from the given file.
346: *
347: * @param filtersFile the file from which filters are read.
348: * @exception BuildException when the file cannot be read.
349: */
350: public synchronized void readFiltersFromFile(File filtersFile)
351: throws BuildException {
352: if (isReference()) {
353: throw tooManyAttributes();
354: }
355: if (!filtersFile.exists()) {
356: handleMissingFile("Could not read filters from file "
357: + filtersFile + " as it doesn't exist.");
358: }
359: if (filtersFile.isFile()) {
360: log("Reading filters from " + filtersFile,
361: Project.MSG_VERBOSE);
362: FileInputStream in = null;
363: try {
364: Properties props = new Properties();
365: in = new FileInputStream(filtersFile);
366: props.load(in);
367:
368: Enumeration e = props.propertyNames();
369: Vector filts = getFilters();
370: while (e.hasMoreElements()) {
371: String strPropName = (String) e.nextElement();
372: String strValue = props.getProperty(strPropName);
373: filts.addElement(new Filter(strPropName, strValue));
374: }
375: } catch (Exception ex) {
376: throw new BuildException(
377: "Could not read filters from file: "
378: + filtersFile);
379: } finally {
380: FileUtils.close(in);
381: }
382: } else {
383: handleMissingFile("Must specify a file rather than a directory in "
384: + "the filtersfile attribute:" + filtersFile);
385: }
386: filterHash = null;
387: }
388:
389: /**
390: * Does replacement on the given string with token matching.
391: * This uses the defined begintoken and endtoken values which default
392: * to @ for both.
393: * This resets the passedTokens and calls iReplaceTokens to
394: * do the actual replacements.
395: *
396: * @param line The line in which to process embedded tokens.
397: * @return The input string after token replacement.
398: */
399: public synchronized String replaceTokens(String line) {
400: return iReplaceTokens(line);
401: }
402:
403: /**
404: * Add a new filter.
405: *
406: * @param filter the filter to be added.
407: */
408: public synchronized void addFilter(Filter filter) {
409: if (isReference()) {
410: throw noChildrenAllowed();
411: }
412: filters.addElement(filter);
413: filterHash = null;
414: }
415:
416: /**
417: * Create a new FiltersFile.
418: *
419: * @return The filtersfile that was created.
420: */
421: public FiltersFile createFiltersfile() {
422: if (isReference()) {
423: throw noChildrenAllowed();
424: }
425: return new FiltersFile();
426: }
427:
428: /**
429: * Add a new filter made from the given token and value.
430: *
431: * @param token The token for the new filter.
432: * @param value The value for the new filter.
433: */
434: public synchronized void addFilter(String token, String value) {
435: if (isReference()) {
436: throw noChildrenAllowed();
437: }
438: addFilter(new Filter(token, value));
439: }
440:
441: /**
442: * Add a Filterset to this filter set.
443: *
444: * @param filterSet the filterset to be added to this filterset
445: */
446: public synchronized void addConfiguredFilterSet(FilterSet filterSet) {
447: if (isReference()) {
448: throw noChildrenAllowed();
449: }
450: for (Enumeration e = filterSet.getFilters().elements(); e
451: .hasMoreElements();) {
452: addFilter((Filter) e.nextElement());
453: }
454: }
455:
456: /**
457: * Test to see if this filter set has filters.
458: *
459: * @return Return true if there are filters in this set.
460: */
461: public synchronized boolean hasFilters() {
462: return getFilters().size() > 0;
463: }
464:
465: /**
466: * Clone the filterset.
467: *
468: * @return a deep clone of this filterset.
469: *
470: * @throws BuildException if the clone cannot be performed.
471: */
472: public synchronized Object clone() throws BuildException {
473: if (isReference()) {
474: return ((FilterSet) getRef()).clone();
475: }
476: try {
477: FilterSet fs = (FilterSet) super .clone();
478: fs.filters = (Vector) getFilters().clone();
479: fs.setProject(getProject());
480: return fs;
481: } catch (CloneNotSupportedException e) {
482: throw new BuildException(e);
483: }
484: }
485:
486: /**
487: * Set the behavior WRT missing filtersfiles.
488: * @param onMissingFiltersFile the OnMissing describing the behavior.
489: */
490: public void setOnMissingFiltersFile(OnMissing onMissingFiltersFile) {
491: this .onMissingFiltersFile = onMissingFiltersFile;
492: }
493:
494: /**
495: * Get the onMissingFiltersFile setting.
496: * @return the OnMissing instance.
497: */
498: public OnMissing getOnMissingFiltersFile() {
499: return onMissingFiltersFile;
500: }
501:
502: /**
503: * Does replacement on the given string with token matching.
504: * This uses the defined begintoken and endtoken values which default
505: * to @ for both.
506: *
507: * @param line The line to process the tokens in.
508: * @return The string with the tokens replaced.
509: */
510: private synchronized String iReplaceTokens(String line) {
511: String beginToken = getBeginToken();
512: String endToken = getEndToken();
513: int index = line.indexOf(beginToken);
514:
515: if (index > -1) {
516: Hashtable tokens = getFilterHash();
517: try {
518: StringBuffer b = new StringBuffer();
519: int i = 0;
520: String token = null;
521: String value = null;
522:
523: while (index > -1) {
524: //can't have zero-length token
525: int endIndex = line.indexOf(endToken, index
526: + beginToken.length() + 1);
527: if (endIndex == -1) {
528: break;
529: }
530: token = line.substring(index + beginToken.length(),
531: endIndex);
532: b.append(line.substring(i, index));
533: if (tokens.containsKey(token)) {
534: value = (String) tokens.get(token);
535: if (recurse && !value.equals(token)) {
536: // we have another token, let's parse it.
537: value = replaceTokens(value, token);
538: }
539: log("Replacing: " + beginToken + token
540: + endToken + " -> " + value,
541: Project.MSG_VERBOSE);
542: b.append(value);
543: i = index + beginToken.length()
544: + token.length() + endToken.length();
545: } else {
546: // just append beginToken and search further
547: b.append(beginToken);
548: i = index + beginToken.length();
549: }
550: index = line.indexOf(beginToken, i);
551: }
552:
553: b.append(line.substring(i));
554: return b.toString();
555: } catch (StringIndexOutOfBoundsException e) {
556: return line;
557: }
558: } else {
559: return line;
560: }
561: }
562:
563: /**
564: * This parses tokens which point to tokens.
565: * It also maintains a list of currently used tokens, so we cannot
566: * get into an infinite loop.
567: * @param line the value / token to parse.
568: * @param parent the parent token (= the token it was parsed from).
569: */
570: private synchronized String replaceTokens(String line, String parent)
571: throws BuildException {
572: String beginToken = getBeginToken();
573: String endToken = getEndToken();
574: if (recurseDepth == 0) {
575: passedTokens = new Vector();
576: }
577: recurseDepth++;
578: if (passedTokens.contains(parent) && !duplicateToken) {
579: duplicateToken = true;
580: System.out
581: .println("Infinite loop in tokens. Currently known tokens : "
582: + passedTokens.toString()
583: + "\nProblem token : "
584: + beginToken
585: + parent
586: + endToken
587: + " called from "
588: + beginToken
589: + passedTokens.lastElement().toString()
590: + endToken);
591: recurseDepth--;
592: return parent;
593: }
594: passedTokens.addElement(parent);
595: String value = iReplaceTokens(line);
596: if (value.indexOf(beginToken) == -1 && !duplicateToken
597: && recurseDepth == 1) {
598: passedTokens = null;
599: } else if (duplicateToken) {
600: // should always be the case...
601: if (passedTokens.size() > 0) {
602: value = (String) passedTokens.remove(passedTokens
603: .size() - 1);
604: if (passedTokens.size() == 0) {
605: value = beginToken + value + endToken;
606: duplicateToken = false;
607: }
608: }
609: }
610: recurseDepth--;
611: return value;
612: }
613:
614: private void handleMissingFile(String message) {
615: switch (onMissingFiltersFile.getIndex()) {
616: case OnMissing.IGNORE_INDEX:
617: return;
618: case OnMissing.FAIL_INDEX:
619: throw new BuildException(message);
620: case OnMissing.WARN_INDEX:
621: log(message, Project.MSG_WARN);
622: return;
623: default:
624: throw new BuildException(
625: "Invalid value for onMissingFiltersFile");
626: }
627: }
628:
629: }
|