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.optional;
020:
021: import java.io.BufferedInputStream;
022: import java.io.BufferedOutputStream;
023: import java.io.File;
024: import java.io.FileInputStream;
025: import java.io.FileOutputStream;
026: import java.io.IOException;
027: import java.text.DateFormat;
028: import java.text.DecimalFormat;
029: import java.text.ParseException;
030: import java.text.SimpleDateFormat;
031: import java.util.Calendar;
032: import java.util.Date;
033: import java.util.Enumeration;
034: import java.util.HashMap;
035: import java.util.Map;
036: import java.util.Properties;
037: import java.util.Vector;
038: import org.apache.tools.ant.BuildException;
039: import org.apache.tools.ant.Task;
040: import org.apache.tools.ant.util.FileUtils;
041: import org.apache.tools.ant.types.EnumeratedAttribute;
042:
043: /**
044: *Modifies settings in a property file.
045: *
046: * <p>
047: *The following is an example of its usage:
048: * <ul><target name="setState"><br>
049: * <ul><property<br>
050: * <ul>name="header"<br>
051: * value="##Generated file - do not modify!"/><br>
052: * <propertyfile file="apropfile.properties" comment="${header}"><br>
053: * <entry key="product.version.major" type="int" value="5"/><br>
054: * <entry key="product.version.minor" type="int" value="0"/><br>
055: * <entry key="product.build.major" type="int" value="0" /><br>
056: * <entry key="product.build.minor" type="int" operation="+" /><br>
057: * <entry key="product.build.date" type="date" value="now" /><br>
058: * <entry key="intSet" type="int" operation="=" value="681"/><br>
059: * <entry key="intDec" type="int" operation="-"/><br>
060: * <entry key="StringEquals" type="string" value="testValue"/><br>
061: * </propertyfile><br></ul>
062: * </target></ul><p>
063: *
064: *The <propertyfile> task must have:<br>
065: * <ul><li>file</li></ul>
066: *Other parameters are:<br>
067: * <ul><li>comment, key, operation, type and value (the final four being
068: * eliminated shortly)</li></ul>
069: *
070: *The <entry> task must have:<br>
071: * <ul><li>key</li></ul>
072: *Other parameters are:<br>
073: * <ul><li>operation</li>
074: * <li>type</li>
075: * <li>value</li>
076: * <li>default</li>
077: * <li>unit</li>
078: * </ul>
079: *
080: *If type is unspecified, it defaults to string
081: *
082: *Parameter values:<br>
083: * <ul><li>operation:</li>
084: * <ul><li>"=" (set -- default)</li>
085: * <li>"-" (dec)</li>
086: * <li>"+" (inc)</li>
087: *
088: * <li>type:</li>
089: * <ul><li>"int"</li>
090: * <li>"date"</li>
091: * <li>"string"</li></ul></ul>
092: *
093: * <li>value:</li>
094: * <ul><li>holds the default value, if the property
095: * was not found in property file</li>
096: * <li>"now" In case of type "date", the
097: * value "now" will be replaced by the current
098: * date/time and used even if a valid date was
099: * found in the property file.</li></ul>
100: *
101: *
102: *String property types can only use the "=" operation.
103: *Int property types can only use the "=", "-" or "+" operations.<p>
104: *
105: *The message property is used for the property file header, with "\\" being
106: *a newline delimiter character.
107: *
108: */
109: public class PropertyFile extends Task {
110:
111: /* ========================================================================
112: *
113: * Instance variables.
114: */
115:
116: // Use this to prepend a message to the properties file
117: private String comment;
118:
119: private Properties properties;
120: private File propertyfile;
121:
122: private Vector entries = new Vector();
123:
124: /* ========================================================================
125: *
126: * Constructors
127: */
128:
129: /* ========================================================================
130: *
131: * Methods
132: */
133:
134: /**
135: * Execute the task.
136: * @throws BuildException on error.
137: */
138: public void execute() throws BuildException {
139: checkParameters();
140: readFile();
141: executeOperation();
142: writeFile();
143: }
144:
145: /**
146: * The entry nested element.
147: * @return an entry nested element to be configured.
148: */
149: public Entry createEntry() {
150: Entry e = new Entry();
151: entries.addElement(e);
152: return e;
153: }
154:
155: private void executeOperation() throws BuildException {
156: for (Enumeration e = entries.elements(); e.hasMoreElements();) {
157: Entry entry = (Entry) e.nextElement();
158: entry.executeOn(properties);
159: }
160: }
161:
162: private void readFile() throws BuildException {
163: // Create the PropertyFile
164: properties = new Properties();
165: try {
166: if (propertyfile.exists()) {
167: log("Updating property file: "
168: + propertyfile.getAbsolutePath());
169: FileInputStream fis = null;
170: try {
171: fis = new FileInputStream(propertyfile);
172: BufferedInputStream bis = new BufferedInputStream(
173: fis);
174: properties.load(bis);
175: } finally {
176: if (fis != null) {
177: fis.close();
178: }
179: }
180: } else {
181: log("Creating new property file: "
182: + propertyfile.getAbsolutePath());
183: FileOutputStream out = null;
184: try {
185: out = new FileOutputStream(propertyfile
186: .getAbsolutePath());
187: out.flush();
188: } finally {
189: if (out != null) {
190: out.close();
191: }
192: }
193: }
194: } catch (IOException ioe) {
195: throw new BuildException(ioe.toString());
196: }
197: }
198:
199: private void checkParameters() throws BuildException {
200: if (!checkParam(propertyfile)) {
201: throw new BuildException("file token must not be null.",
202: getLocation());
203: }
204: }
205:
206: /**
207: * Location of the property file to be edited; required.
208: * @param file the property file.
209: */
210: public void setFile(File file) {
211: propertyfile = file;
212: }
213:
214: /**
215: * optional header comment for the file
216: * @param hdr the string to use for the comment.
217: */
218: public void setComment(String hdr) {
219: comment = hdr;
220: }
221:
222: private void writeFile() throws BuildException {
223: BufferedOutputStream bos = null;
224: try {
225: bos = new BufferedOutputStream(new FileOutputStream(
226: propertyfile));
227: properties.store(bos, comment);
228: } catch (IOException ioe) {
229: throw new BuildException(ioe, getLocation());
230: } finally {
231: FileUtils.close(bos);
232: }
233: }
234:
235: private boolean checkParam(File param) {
236: return !(param == null);
237: }
238:
239: /**
240: * Instance of this class represents nested elements of
241: * a task propertyfile.
242: */
243: public static class Entry {
244: private static final int DEFAULT_INT_VALUE = 0;
245: private static final String DEFAULT_DATE_VALUE = "now";
246: private static final String DEFAULT_STRING_VALUE = "";
247:
248: private String key = null;
249: private int type = Type.STRING_TYPE;
250: private int operation = Operation.EQUALS_OPER;
251: private String value = null;
252: private String defaultValue = null;
253: private String newValue = null;
254: private String pattern = null;
255: private int field = Calendar.DATE;
256:
257: /**
258: * Name of the property name/value pair
259: * @param value the key.
260: */
261: public void setKey(String value) {
262: this .key = value;
263: }
264:
265: /**
266: * Value to set (=), to add (+) or subtract (-)
267: * @param value the value.
268: */
269: public void setValue(String value) {
270: this .value = value;
271: }
272:
273: /**
274: * operation to apply.
275: * "+" or "="
276: *(default) for all datatypes; "-" for date and int only)\.
277: * @param value the operation enumerated value.
278: */
279: public void setOperation(Operation value) {
280: this .operation = Operation.toOperation(value.getValue());
281: }
282:
283: /**
284: * Regard the value as : int, date or string (default)
285: * @param value the type enumerated value.
286: */
287: public void setType(Type value) {
288: this .type = Type.toType(value.getValue());
289: }
290:
291: /**
292: * Initial value to set for a property if it is not
293: * already defined in the property file.
294: * For type date, an additional keyword is allowed: "now"
295: * @param value the default value.
296: */
297: public void setDefault(String value) {
298: this .defaultValue = value;
299: }
300:
301: /**
302: * For int and date type only. If present, Values will
303: * be parsed and formatted accordingly.
304: * @param value the pattern to use.
305: */
306: public void setPattern(String value) {
307: this .pattern = value;
308: }
309:
310: /**
311: * The unit of the value to be applied to date +/- operations.
312: * Valid Values are:
313: * <ul>
314: * <li>millisecond</li>
315: * <li>second</li>
316: * <li>minute</li>
317: * <li>hour</li>
318: * <li>day (default)</li>
319: * <li>week</li>
320: * <li>month</li>
321: * <li>year</li>
322: * </ul>
323: * This only applies to date types using a +/- operation.
324: * @param unit the unit enumerated value.
325: * @since Ant 1.5
326: */
327: public void setUnit(PropertyFile.Unit unit) {
328: field = unit.getCalendarField();
329: }
330:
331: /**
332: * Apply the nested element to the properties.
333: * @param props the properties to apply the entry on.
334: * @throws BuildException if there is an error.
335: */
336: protected void executeOn(Properties props)
337: throws BuildException {
338: checkParameters();
339:
340: // type may be null because it wasn't set
341: String oldValue = (String) props.get(key);
342: try {
343: if (type == Type.INTEGER_TYPE) {
344: executeInteger(oldValue);
345: } else if (type == Type.DATE_TYPE) {
346: executeDate(oldValue);
347: } else if (type == Type.STRING_TYPE) {
348: executeString(oldValue);
349: } else {
350: throw new BuildException("Unknown operation type: "
351: + type);
352: }
353: } catch (NullPointerException npe) {
354: // Default to string type
355: // which means do nothing
356: npe.printStackTrace();
357: }
358:
359: if (newValue == null) {
360: newValue = "";
361: }
362:
363: // Insert as a string by default
364: props.put(key, newValue);
365: }
366:
367: /**
368: * Handle operations for type <code>date</code>.
369: *
370: * @param oldValue the current value read from the property file or
371: * <code>null</code> if the <code>key</code> was
372: * not contained in the property file.
373: */
374: private void executeDate(String oldValue) throws BuildException {
375: Calendar currentValue = Calendar.getInstance();
376:
377: if (pattern == null) {
378: pattern = "yyyy/MM/dd HH:mm";
379: }
380: DateFormat fmt = new SimpleDateFormat(pattern);
381:
382: String currentStringValue = getCurrentValue(oldValue);
383: if (currentStringValue == null) {
384: currentStringValue = DEFAULT_DATE_VALUE;
385: }
386:
387: if ("now".equals(currentStringValue)) {
388: currentValue.setTime(new Date());
389: } else {
390: try {
391: currentValue.setTime(fmt.parse(currentStringValue));
392: } catch (ParseException pe) {
393: // swallow
394: }
395: }
396:
397: if (operation != Operation.EQUALS_OPER) {
398: int offset = 0;
399: try {
400: offset = Integer.parseInt(value);
401: if (operation == Operation.DECREMENT_OPER) {
402: offset = -1 * offset;
403: }
404: } catch (NumberFormatException e) {
405: throw new BuildException("Value not an integer on "
406: + key);
407: }
408: currentValue.add(field, offset);
409: }
410:
411: newValue = fmt.format(currentValue.getTime());
412: }
413:
414: /**
415: * Handle operations for type <code>int</code>.
416: *
417: * @param oldValue the current value read from the property file or
418: * <code>null</code> if the <code>key</code> was
419: * not contained in the property file.
420: */
421: private void executeInteger(String oldValue)
422: throws BuildException {
423: int currentValue = DEFAULT_INT_VALUE;
424: int newV = DEFAULT_INT_VALUE;
425:
426: DecimalFormat fmt = (pattern != null) ? new DecimalFormat(
427: pattern) : new DecimalFormat();
428: try {
429: String curval = getCurrentValue(oldValue);
430: if (curval != null) {
431: currentValue = fmt.parse(curval).intValue();
432: } else {
433: currentValue = 0;
434: }
435: } catch (NumberFormatException nfe) {
436: // swallow
437: } catch (ParseException pe) {
438: // swallow
439: }
440:
441: if (operation == Operation.EQUALS_OPER) {
442: newV = currentValue;
443: } else {
444: int operationValue = 1;
445: if (value != null) {
446: try {
447: operationValue = fmt.parse(value).intValue();
448: } catch (NumberFormatException nfe) {
449: // swallow
450: } catch (ParseException pe) {
451: // swallow
452: }
453: }
454:
455: if (operation == Operation.INCREMENT_OPER) {
456: newV = currentValue + operationValue;
457: } else if (operation == Operation.DECREMENT_OPER) {
458: newV = currentValue - operationValue;
459: }
460: }
461:
462: this .newValue = fmt.format(newV);
463: }
464:
465: /**
466: * Handle operations for type <code>string</code>.
467: *
468: * @param oldValue the current value read from the property file or
469: * <code>null</code> if the <code>key</code> was
470: * not contained in the property file.
471: */
472: private void executeString(String oldValue)
473: throws BuildException {
474: String newV = DEFAULT_STRING_VALUE;
475:
476: String currentValue = getCurrentValue(oldValue);
477:
478: if (currentValue == null) {
479: currentValue = DEFAULT_STRING_VALUE;
480: }
481:
482: if (operation == Operation.EQUALS_OPER) {
483: newV = currentValue;
484: } else if (operation == Operation.INCREMENT_OPER) {
485: newV = currentValue + value;
486: }
487: this .newValue = newV;
488: }
489:
490: /**
491: * Check if parameter combinations can be supported
492: * @todo make sure the 'unit' attribute is only specified on date
493: * fields
494: */
495: private void checkParameters() throws BuildException {
496: if (type == Type.STRING_TYPE
497: && operation == Operation.DECREMENT_OPER) {
498: throw new BuildException(
499: "- is not supported for string "
500: + "properties (key:" + key + ")");
501: }
502: if (value == null && defaultValue == null) {
503: throw new BuildException(
504: "\"value\" and/or \"default\" "
505: + "attribute must be specified (key:"
506: + key + ")");
507: }
508: if (key == null) {
509: throw new BuildException("key is mandatory");
510: }
511: if (type == Type.STRING_TYPE && pattern != null) {
512: throw new BuildException(
513: "pattern is not supported for string "
514: + "properties (key:" + key + ")");
515: }
516: }
517:
518: private String getCurrentValue(String oldValue) {
519: String ret = null;
520: if (operation == Operation.EQUALS_OPER) {
521: // If only value is specified, the property is set to it
522: // regardless of its previous value.
523: if (value != null && defaultValue == null) {
524: ret = value;
525: }
526:
527: // If only default is specified and the property previously
528: // existed in the property file, it is unchanged.
529: if (value == null && defaultValue != null
530: && oldValue != null) {
531: ret = oldValue;
532: }
533:
534: // If only default is specified and the property did not
535: // exist in the property file, the property is set to default.
536: if (value == null && defaultValue != null
537: && oldValue == null) {
538: ret = defaultValue;
539: }
540:
541: // If value and default are both specified and the property
542: // previously existed in the property file, the property
543: // is set to value.
544: if (value != null && defaultValue != null
545: && oldValue != null) {
546: ret = value;
547: }
548:
549: // If value and default are both specified and the property
550: // did not exist in the property file, the property is set
551: // to default.
552: if (value != null && defaultValue != null
553: && oldValue == null) {
554: ret = defaultValue;
555: }
556: } else {
557: ret = (oldValue == null) ? defaultValue : oldValue;
558: }
559:
560: return ret;
561: }
562:
563: /**
564: * Enumerated attribute with the values "+", "-", "="
565: */
566: public static class Operation extends EnumeratedAttribute {
567:
568: // Property type operations
569: /** + */
570: public static final int INCREMENT_OPER = 0;
571: /** - */
572: public static final int DECREMENT_OPER = 1;
573: /** = */
574: public static final int EQUALS_OPER = 2;
575:
576: /** {@inheritDoc}. */
577: public String[] getValues() {
578: return new String[] { "+", "-", "=" };
579: }
580:
581: /**
582: * Convert string to index.
583: * @param oper the string to convert.
584: * @return the index.
585: */
586: public static int toOperation(String oper) {
587: if ("+".equals(oper)) {
588: return INCREMENT_OPER;
589: } else if ("-".equals(oper)) {
590: return DECREMENT_OPER;
591: }
592: return EQUALS_OPER;
593: }
594: }
595:
596: /**
597: * Enumerated attribute with the values "int", "date" and "string".
598: */
599: public static class Type extends EnumeratedAttribute {
600:
601: // Property types
602: /** int */
603: public static final int INTEGER_TYPE = 0;
604: /** date */
605: public static final int DATE_TYPE = 1;
606: /** string */
607: public static final int STRING_TYPE = 2;
608:
609: /** {@inheritDoc} */
610: public String[] getValues() {
611: return new String[] { "int", "date", "string" };
612: }
613:
614: /**
615: * Convert string to index.
616: * @param type the string to convert.
617: * @return the index.
618: */
619: public static int toType(String type) {
620: if ("int".equals(type)) {
621: return INTEGER_TYPE;
622: } else if ("date".equals(type)) {
623: return DATE_TYPE;
624: }
625: return STRING_TYPE;
626: }
627: }
628: }
629:
630: /**
631: * Borrowed from Tstamp
632: * @todo share all this time stuff across many tasks as a datetime datatype
633: * @since Ant 1.5
634: */
635: public static class Unit extends EnumeratedAttribute {
636:
637: private static final String MILLISECOND = "millisecond";
638: private static final String SECOND = "second";
639: private static final String MINUTE = "minute";
640: private static final String HOUR = "hour";
641: private static final String DAY = "day";
642: private static final String WEEK = "week";
643: private static final String MONTH = "month";
644: private static final String YEAR = "year";
645:
646: private static final String[] UNITS = { MILLISECOND, SECOND,
647: MINUTE, HOUR, DAY, WEEK, MONTH, YEAR };
648:
649: private Map calendarFields = new HashMap();
650:
651: /** no arg constructor */
652: public Unit() {
653: calendarFields.put(MILLISECOND, new Integer(
654: Calendar.MILLISECOND));
655: calendarFields.put(SECOND, new Integer(Calendar.SECOND));
656: calendarFields.put(MINUTE, new Integer(Calendar.MINUTE));
657: calendarFields.put(HOUR, new Integer(Calendar.HOUR_OF_DAY));
658: calendarFields.put(DAY, new Integer(Calendar.DATE));
659: calendarFields
660: .put(WEEK, new Integer(Calendar.WEEK_OF_YEAR));
661: calendarFields.put(MONTH, new Integer(Calendar.MONTH));
662: calendarFields.put(YEAR, new Integer(Calendar.YEAR));
663: }
664:
665: /**
666: * Convert the value to a Calendar field index.
667: * @return the calander value.
668: */
669: public int getCalendarField() {
670: String key = getValue().toLowerCase();
671: Integer i = (Integer) calendarFields.get(key);
672: return i.intValue();
673: }
674:
675: /** {@inheritDoc}. */
676: public String[] getValues() {
677: return UNITS;
678: }
679: }
680: }
|