1. /*
  2. * Copyright 2000-2004 The Apache Software Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.apache.tools.ant.taskdefs.optional;
  18. import java.io.BufferedInputStream;
  19. import java.io.BufferedOutputStream;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FileOutputStream;
  23. import java.io.IOException;
  24. import java.text.DateFormat;
  25. import java.text.DecimalFormat;
  26. import java.text.ParseException;
  27. import java.text.SimpleDateFormat;
  28. import java.util.Calendar;
  29. import java.util.Date;
  30. import java.util.Enumeration;
  31. import java.util.Hashtable;
  32. import java.util.Properties;
  33. import java.util.Vector;
  34. import org.apache.tools.ant.BuildException;
  35. import org.apache.tools.ant.Task;
  36. import org.apache.tools.ant.types.EnumeratedAttribute;
  37. /**
  38. *Modifies settings in a property file.
  39. *
  40. * <p>
  41. *The following is an example of its usage:
  42. * <ul><target name="setState"><br>
  43. * <ul><property<br>
  44. * <ul>name="header"<br>
  45. * value="##Generated file - do not modify!"/><br>
  46. * <propertyfile file="apropfile.properties" comment="${header}"><br>
  47. * <entry key="product.version.major" type="int" value="5"/><br>
  48. * <entry key="product.version.minor" type="int" value="0"/><br>
  49. * <entry key="product.build.major" type="int" value="0" /><br>
  50. * <entry key="product.build.minor" type="int" operation="+" /><br>
  51. * <entry key="product.build.date" type="date" value="now" /><br>
  52. * <entry key="intSet" type="int" operation="=" value="681"/><br>
  53. * <entry key="intDec" type="int" operation="-"/><br>
  54. * <entry key="StringEquals" type="string" value="testValue"/><br>
  55. * </propertyfile><br></ul>
  56. * </target></ul><p>
  57. *
  58. *The <propertyfile> task must have:<br>
  59. * <ul><li>file</li></ul>
  60. *Other parameters are:<br>
  61. * <ul><li>comment, key, operation, type and value (the final four being
  62. * eliminated shortly)</li></ul>
  63. *
  64. *The <entry> task must have:<br>
  65. * <ul><li>key</li></ul>
  66. *Other parameters are:<br>
  67. * <ul><li>operation</li>
  68. * <li>type</li>
  69. * <li>value</li>
  70. * <li>default</li>
  71. * <li>unit</li>
  72. * </ul>
  73. *
  74. *If type is unspecified, it defaults to string
  75. *
  76. *Parameter values:<br>
  77. * <ul><li>operation:</li>
  78. * <ul><li>"=" (set -- default)</li>
  79. * <li>"-" (dec)</li>
  80. * <li>"+" (inc)</li>
  81. *
  82. * <li>type:</li>
  83. * <ul><li>"int"</li>
  84. * <li>"date"</li>
  85. * <li>"string"</li></ul></ul>
  86. *
  87. * <li>value:</li>
  88. * <ul><li>holds the default value, if the property
  89. * was not found in property file</li>
  90. * <li>"now" In case of type "date", the
  91. * value "now" will be replaced by the current
  92. * date/time and used even if a valid date was
  93. * found in the property file.</li></ul>
  94. *
  95. *
  96. *String property types can only use the "=" operation.
  97. *Int property types can only use the "=", "-" or "+" operations.<p>
  98. *
  99. *The message property is used for the property file header, with "\\" being
  100. *a newline delimiter character.
  101. *
  102. */
  103. public class PropertyFile extends Task {
  104. /* ========================================================================
  105. *
  106. * Instance variables.
  107. */
  108. // Use this to prepend a message to the properties file
  109. private String comment;
  110. private Properties properties;
  111. private File propertyfile;
  112. private Vector entries = new Vector();
  113. /* ========================================================================
  114. *
  115. * Constructors
  116. */
  117. /* ========================================================================
  118. *
  119. * Methods
  120. */
  121. public void execute() throws BuildException {
  122. checkParameters();
  123. readFile();
  124. executeOperation();
  125. writeFile();
  126. }
  127. public Entry createEntry() {
  128. Entry e = new Entry();
  129. entries.addElement(e);
  130. return e;
  131. }
  132. private void executeOperation() throws BuildException {
  133. for (Enumeration e = entries.elements(); e.hasMoreElements();) {
  134. Entry entry = (Entry) e.nextElement();
  135. entry.executeOn(properties);
  136. }
  137. }
  138. private void readFile() throws BuildException {
  139. // Create the PropertyFile
  140. properties = new Properties();
  141. try {
  142. if (propertyfile.exists()) {
  143. log("Updating property file: "
  144. + propertyfile.getAbsolutePath());
  145. FileInputStream fis = null;
  146. try {
  147. fis = new FileInputStream(propertyfile);
  148. BufferedInputStream bis = new BufferedInputStream(fis);
  149. properties.load(bis);
  150. } finally {
  151. if (fis != null) {
  152. fis.close();
  153. }
  154. }
  155. } else {
  156. log("Creating new property file: "
  157. + propertyfile.getAbsolutePath());
  158. FileOutputStream out = null;
  159. try {
  160. out = new FileOutputStream(propertyfile.getAbsolutePath());
  161. out.flush();
  162. } finally {
  163. if (out != null) {
  164. out.close();
  165. }
  166. }
  167. }
  168. } catch (IOException ioe) {
  169. throw new BuildException(ioe.toString());
  170. }
  171. }
  172. private void checkParameters() throws BuildException {
  173. if (!checkParam(propertyfile)) {
  174. throw new BuildException("file token must not be null.", getLocation());
  175. }
  176. }
  177. /**
  178. * Location of the property file to be edited; required.
  179. */
  180. public void setFile(File file) {
  181. propertyfile = file;
  182. }
  183. /**
  184. * optional header comment for the file
  185. */
  186. public void setComment(String hdr) {
  187. comment = hdr;
  188. }
  189. private void writeFile() throws BuildException {
  190. BufferedOutputStream bos = null;
  191. try {
  192. bos = new BufferedOutputStream(new FileOutputStream(propertyfile));
  193. properties.store(bos, comment);
  194. } catch (IOException ioe) {
  195. throw new BuildException(ioe, getLocation());
  196. } finally {
  197. if (bos != null) {
  198. try {
  199. bos.close();
  200. } catch (IOException ioex) {
  201. // ignore
  202. }
  203. }
  204. }
  205. }
  206. private boolean checkParam(File param) {
  207. return !(param == null);
  208. }
  209. /**
  210. * Instance of this class represents nested elements of
  211. * a task propertyfile.
  212. */
  213. public static class Entry {
  214. private static final int DEFAULT_INT_VALUE = 0;
  215. private static final String DEFAULT_DATE_VALUE = "now";
  216. private static final String DEFAULT_STRING_VALUE = "";
  217. private String key = null;
  218. private int type = Type.STRING_TYPE;
  219. private int operation = Operation.EQUALS_OPER;
  220. private String value = null;
  221. private String defaultValue = null;
  222. private String newValue = null;
  223. private String pattern = null;
  224. private int field = Calendar.DATE;
  225. /**
  226. * Name of the property name/value pair
  227. */
  228. public void setKey(String value) {
  229. this.key = value;
  230. }
  231. /**
  232. * Value to set (=), to add (+) or subtract (-)
  233. */
  234. public void setValue(String value) {
  235. this.value = value;
  236. }
  237. /**
  238. * operation to apply.
  239. * "+" or "="
  240. *(default) for all datatypes; "-" for date and int only)\.
  241. */
  242. public void setOperation(Operation value) {
  243. this.operation = Operation.toOperation(value.getValue());
  244. }
  245. /**
  246. * Regard the value as : int, date or string (default)
  247. */
  248. public void setType(Type value) {
  249. this.type = Type.toType(value.getValue());
  250. }
  251. /**
  252. * Initial value to set for a property if it is not
  253. * already defined in the property file.
  254. * For type date, an additional keyword is allowed: "now"
  255. */
  256. public void setDefault(String value) {
  257. this.defaultValue = value;
  258. }
  259. /**
  260. * For int and date type only. If present, Values will
  261. * be parsed and formatted accordingly.
  262. */
  263. public void setPattern(String value) {
  264. this.pattern = value;
  265. }
  266. /**
  267. * The unit of the value to be applied to date +/- operations.
  268. * Valid Values are:
  269. * <ul>
  270. * <li>millisecond</li>
  271. * <li>second</li>
  272. * <li>minute</li>
  273. * <li>hour</li>
  274. * <li>day (default)</li>
  275. * <li>week</li>
  276. * <li>month</li>
  277. * <li>year</li>
  278. * </ul>
  279. * This only applies to date types using a +/- operation.
  280. * @since Ant 1.5
  281. */
  282. public void setUnit(PropertyFile.Unit unit) {
  283. field = unit.getCalendarField();
  284. }
  285. protected void executeOn(Properties props) throws BuildException {
  286. checkParameters();
  287. // type may be null because it wasn't set
  288. String oldValue = (String) props.get(key);
  289. try {
  290. if (type == Type.INTEGER_TYPE) {
  291. executeInteger(oldValue);
  292. } else if (type == Type.DATE_TYPE) {
  293. executeDate(oldValue);
  294. } else if (type == Type.STRING_TYPE) {
  295. executeString(oldValue);
  296. } else {
  297. throw new BuildException("Unknown operation type: "
  298. + type);
  299. }
  300. } catch (NullPointerException npe) {
  301. // Default to string type
  302. // which means do nothing
  303. npe.printStackTrace();
  304. }
  305. if (newValue == null) {
  306. newValue = "";
  307. }
  308. // Insert as a string by default
  309. props.put(key, newValue);
  310. }
  311. /**
  312. * Handle operations for type <code>date</code>.
  313. *
  314. * @param oldValue the current value read from the property file or
  315. * <code>null</code> if the <code>key</code> was
  316. * not contained in the property file.
  317. */
  318. private void executeDate(String oldValue) throws BuildException {
  319. Calendar currentValue = Calendar.getInstance();
  320. if (pattern == null) {
  321. pattern = "yyyy/MM/dd HH:mm";
  322. }
  323. DateFormat fmt = new SimpleDateFormat(pattern);
  324. String currentStringValue = getCurrentValue(oldValue);
  325. if (currentStringValue == null) {
  326. currentStringValue = DEFAULT_DATE_VALUE;
  327. }
  328. if ("now".equals(currentStringValue)) {
  329. currentValue.setTime(new Date());
  330. } else {
  331. try {
  332. currentValue.setTime(fmt.parse(currentStringValue));
  333. } catch (ParseException pe) {
  334. // swallow
  335. }
  336. }
  337. if (operation != Operation.EQUALS_OPER) {
  338. int offset = 0;
  339. try {
  340. offset = Integer.parseInt(value);
  341. if (operation == Operation.DECREMENT_OPER) {
  342. offset = -1 * offset;
  343. }
  344. } catch (NumberFormatException e) {
  345. throw new BuildException("Value not an integer on " + key);
  346. }
  347. currentValue.add(field, offset);
  348. }
  349. newValue = fmt.format(currentValue.getTime());
  350. }
  351. /**
  352. * Handle operations for type <code>int</code>.
  353. *
  354. * @param oldValue the current value read from the property file or
  355. * <code>null</code> if the <code>key</code> was
  356. * not contained in the property file.
  357. */
  358. private void executeInteger(String oldValue) throws BuildException {
  359. int currentValue = DEFAULT_INT_VALUE;
  360. int newValue = DEFAULT_INT_VALUE;
  361. DecimalFormat fmt = (pattern != null) ? new DecimalFormat(pattern)
  362. : new DecimalFormat();
  363. try {
  364. String curval = getCurrentValue(oldValue);
  365. if (curval != null) {
  366. currentValue = fmt.parse(curval).intValue();
  367. } else {
  368. currentValue = 0;
  369. }
  370. } catch (NumberFormatException nfe) {
  371. // swallow
  372. } catch (ParseException pe) {
  373. // swallow
  374. }
  375. if (operation == Operation.EQUALS_OPER) {
  376. newValue = currentValue;
  377. } else {
  378. int operationValue = 1;
  379. if (value != null) {
  380. try {
  381. operationValue = fmt.parse(value).intValue();
  382. } catch (NumberFormatException nfe) {
  383. // swallow
  384. } catch (ParseException pe) {
  385. // swallow
  386. }
  387. }
  388. if (operation == Operation.INCREMENT_OPER) {
  389. newValue = currentValue + operationValue;
  390. } else if (operation == Operation.DECREMENT_OPER) {
  391. newValue = currentValue - operationValue;
  392. }
  393. }
  394. this.newValue = fmt.format(newValue);
  395. }
  396. /**
  397. * Handle operations for type <code>string</code>.
  398. *
  399. * @param oldValue the current value read from the property file or
  400. * <code>null</code> if the <code>key</code> was
  401. * not contained in the property file.
  402. */
  403. private void executeString(String oldValue) throws BuildException {
  404. String newValue = DEFAULT_STRING_VALUE;
  405. String currentValue = getCurrentValue(oldValue);
  406. if (currentValue == null) {
  407. currentValue = DEFAULT_STRING_VALUE;
  408. }
  409. if (operation == Operation.EQUALS_OPER) {
  410. newValue = currentValue;
  411. } else if (operation == Operation.INCREMENT_OPER) {
  412. newValue = currentValue + value;
  413. }
  414. this.newValue = newValue;
  415. }
  416. /**
  417. * Check if parameter combinations can be supported
  418. * @todo make sure the 'unit' attribute is only specified on date
  419. * fields
  420. */
  421. private void checkParameters() throws BuildException {
  422. if (type == Type.STRING_TYPE
  423. && operation == Operation.DECREMENT_OPER) {
  424. throw new BuildException("- is not supported for string "
  425. + "properties (key:" + key + ")");
  426. }
  427. if (value == null && defaultValue == null) {
  428. throw new BuildException("\"value\" and/or \"default\" "
  429. + "attribute must be specified (key:" + key + ")");
  430. }
  431. if (key == null) {
  432. throw new BuildException("key is mandatory");
  433. }
  434. if (type == Type.STRING_TYPE && pattern != null) {
  435. throw new BuildException("pattern is not supported for string "
  436. + "properties (key:" + key + ")");
  437. }
  438. }
  439. private String getCurrentValue(String oldValue) {
  440. String ret = null;
  441. if (operation == Operation.EQUALS_OPER) {
  442. // If only value is specified, the property is set to it
  443. // regardless of its previous value.
  444. if (value != null && defaultValue == null) {
  445. ret = value;
  446. }
  447. // If only default is specified and the property previously
  448. // existed in the property file, it is unchanged.
  449. if (value == null && defaultValue != null && oldValue != null) {
  450. ret = oldValue;
  451. }
  452. // If only default is specified and the property did not
  453. // exist in the property file, the property is set to default.
  454. if (value == null && defaultValue != null && oldValue == null) {
  455. ret = defaultValue;
  456. }
  457. // If value and default are both specified and the property
  458. // previously existed in the property file, the property
  459. // is set to value.
  460. if (value != null && defaultValue != null && oldValue != null) {
  461. ret = value;
  462. }
  463. // If value and default are both specified and the property
  464. // did not exist in the property file, the property is set
  465. // to default.
  466. if (value != null && defaultValue != null && oldValue == null) {
  467. ret = defaultValue;
  468. }
  469. } else {
  470. ret = (oldValue == null) ? defaultValue : oldValue;
  471. }
  472. return ret;
  473. }
  474. /**
  475. * Enumerated attribute with the values "+", "-", "="
  476. */
  477. public static class Operation extends EnumeratedAttribute {
  478. // Property type operations
  479. public static final int INCREMENT_OPER = 0;
  480. public static final int DECREMENT_OPER = 1;
  481. public static final int EQUALS_OPER = 2;
  482. public String[] getValues() {
  483. return new String[] {"+", "-", "="};
  484. }
  485. public static int toOperation(String oper) {
  486. if ("+".equals(oper)) {
  487. return INCREMENT_OPER;
  488. } else if ("-".equals(oper)) {
  489. return DECREMENT_OPER;
  490. }
  491. return EQUALS_OPER;
  492. }
  493. }
  494. /**
  495. * Enumerated attribute with the values "int", "date" and "string".
  496. */
  497. public static class Type extends EnumeratedAttribute {
  498. // Property types
  499. public static final int INTEGER_TYPE = 0;
  500. public static final int DATE_TYPE = 1;
  501. public static final int STRING_TYPE = 2;
  502. public String[] getValues() {
  503. return new String[] {"int", "date", "string"};
  504. }
  505. public static int toType(String type) {
  506. if ("int".equals(type)) {
  507. return INTEGER_TYPE;
  508. } else if ("date".equals(type)) {
  509. return DATE_TYPE;
  510. }
  511. return STRING_TYPE;
  512. }
  513. }
  514. }
  515. /**
  516. * Borrowed from Tstamp
  517. * @todo share all this time stuff across many tasks as a datetime datatype
  518. * @since Ant 1.5
  519. */
  520. public static class Unit extends EnumeratedAttribute {
  521. private static final String MILLISECOND = "millisecond";
  522. private static final String SECOND = "second";
  523. private static final String MINUTE = "minute";
  524. private static final String HOUR = "hour";
  525. private static final String DAY = "day";
  526. private static final String WEEK = "week";
  527. private static final String MONTH = "month";
  528. private static final String YEAR = "year";
  529. private static final String[] UNITS
  530. = {MILLISECOND, SECOND, MINUTE, HOUR,
  531. DAY, WEEK, MONTH, YEAR };
  532. private Hashtable calendarFields = new Hashtable();
  533. public Unit() {
  534. calendarFields.put(MILLISECOND,
  535. new Integer(Calendar.MILLISECOND));
  536. calendarFields.put(SECOND, new Integer(Calendar.SECOND));
  537. calendarFields.put(MINUTE, new Integer(Calendar.MINUTE));
  538. calendarFields.put(HOUR, new Integer(Calendar.HOUR_OF_DAY));
  539. calendarFields.put(DAY, new Integer(Calendar.DATE));
  540. calendarFields.put(WEEK, new Integer(Calendar.WEEK_OF_YEAR));
  541. calendarFields.put(MONTH, new Integer(Calendar.MONTH));
  542. calendarFields.put(YEAR, new Integer(Calendar.YEAR));
  543. }
  544. public int getCalendarField() {
  545. String key = getValue().toLowerCase();
  546. Integer i = (Integer) calendarFields.get(key);
  547. return i.intValue();
  548. }
  549. public String[] getValues() {
  550. return UNITS;
  551. }
  552. }
  553. }