You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

LayoutPreservingProperties.java 25 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. */
  18. package org.apache.tools.ant.util;
  19. import java.io.BufferedReader;
  20. import java.io.ByteArrayInputStream;
  21. import java.io.File;
  22. import java.io.FileOutputStream;
  23. import java.io.IOException;
  24. import java.io.InputStream;
  25. import java.io.InputStreamReader;
  26. import java.io.OutputStream;
  27. import java.io.OutputStreamWriter;
  28. import java.io.PrintStream;
  29. import java.util.ArrayList;
  30. import java.util.Date;
  31. import java.util.HashMap;
  32. import java.util.Iterator;
  33. import java.util.Properties;
  34. /**
  35. * <p>A Properties collection which preserves comments and whitespace
  36. * present in the input stream from which it was loaded.</p>
  37. * <p>The class defers the usual work of the <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>
  38. * class to there, but it also keeps track of the contents of the
  39. * input stream from which it was loaded (if applicable), so that in can
  40. * write out the properties in as close a form as possible to the input.</p>
  41. * If no changes occur to property values, the output should be the same
  42. * as the input, except for the leading date stamp, as normal for a
  43. * properties file. Properties added are appended to the file. Properties
  44. * whose values are changed are changed in place. Properties that are
  45. * removed are excised. If the <code>removeComments</code> flag is set,
  46. * then the comments immediately preceding the property are also removed.</p>
  47. * <p>If a second set of properties is loaded into an existing set, the
  48. * lines of the second set are added to the end. Note however, that if a
  49. * property already stored is present in a stream subsequently loaded, then
  50. * that property is removed before the new value is set. For example,
  51. * consider the file</p>
  52. * <pre> # the first line
  53. * alpha=one
  54. *
  55. * # the second line
  56. * beta=two</pre>
  57. * <p>This file is loaded, and then the following is also loaded into the
  58. * same <code>LayoutPreservingProperties</code> object</p>
  59. * <pre> # association
  60. * beta=band
  61. *
  62. * # and finally
  63. * gamma=rays</pre>
  64. * </p>The resulting collection sequence of logical lines depends on whether
  65. * or not <code>removeComments</code> was set at the time the second stream
  66. * is loaded. If it is set, then the resulting list of lines is</p>
  67. * <pre> # the first line
  68. * alpha=one
  69. *
  70. * # association
  71. * beta=band
  72. *
  73. * # and finally
  74. * gamma=rays</pre>
  75. * <p>If the flag is not set, then the comment "the second line" is retained,
  76. * although the key-value pair <code>beta=two</code> is removed.</p>
  77. */
  78. public class LayoutPreservingProperties extends Properties {
  79. private static final String LS = System.getProperty("line.separator");
  80. /**
  81. * Logical lines have escaping and line continuation taken care of. Comments
  82. * and blank lines are logical lines; they are not removed.
  83. */
  84. private ArrayList logicalLines = new ArrayList();
  85. /**
  86. * Position in the <code>logicalLines</code> list, keyed by property name.
  87. */
  88. private HashMap keyedPairLines = new HashMap();
  89. /**
  90. * Flag to indicate that, when we remove a property from the file, we
  91. * also want to remove the comments that precede it.
  92. */
  93. private boolean removeComments;
  94. /**
  95. * Create a new, empty, Properties collection, with no defaults.
  96. */
  97. public LayoutPreservingProperties() {
  98. super();
  99. }
  100. /**
  101. * Create a new, empty, Properties collection, with the specified defaults.
  102. * @param defaults the default property values
  103. */
  104. public LayoutPreservingProperties(Properties defaults) {
  105. super(defaults);
  106. }
  107. /**
  108. * Returns <code>true</code> if comments are removed along with properties, or
  109. * <code>false</code> otherwise. If <code>true</code>, then when a property is
  110. * removed, the comment preceding it in the original file is removed also.
  111. * @return <code>true</code> if leading comments are removed when a property is
  112. * removed; <code>false</code> otherwise
  113. */
  114. public boolean isRemoveComments() {
  115. return removeComments;
  116. }
  117. /**
  118. * Sets the behaviour for comments accompanying properties that are being
  119. * removed. If <code>true</code>, then when a property is removed, the comment
  120. * preceding it in the original file is removed also.
  121. * @param val <code>true</code> if leading comments are to be removed when a property is
  122. * removed; <code>false</code> otherwise
  123. */
  124. public void setRemoveComments(boolean val) {
  125. removeComments = val;
  126. }
  127. public void load(InputStream inStream) throws IOException {
  128. String s = readLines(inStream);
  129. byte[] ba = s.getBytes("ISO-8859-1");
  130. ByteArrayInputStream bais = new ByteArrayInputStream(ba);
  131. super.load(bais);
  132. }
  133. public Object put(Object key, Object value) throws NullPointerException {
  134. Object obj = super.put(key, value);
  135. // the above call will have failed if key or value are null
  136. innerSetProperty(key.toString(), value.toString());
  137. return obj;
  138. }
  139. public Object setProperty(String key, String value) throws NullPointerException {
  140. Object obj = super.setProperty(key, value);
  141. // the above call will have failed if key or value are null
  142. innerSetProperty(key, value);
  143. return obj;
  144. }
  145. /**
  146. * Store a new key-value pair, or add a new one. The normal functionality is
  147. * taken care of by the superclass in the call to {@link #setProperty}; this
  148. * method takes care of this classes extensions.
  149. * @param key the key of the property to be stored
  150. * @param value the value to be stored
  151. */
  152. private void innerSetProperty(String key, String value) {
  153. value = escapeValue(value);
  154. if (keyedPairLines.containsKey(key)) {
  155. Integer i = (Integer) keyedPairLines.get(key);
  156. Pair p = (Pair) logicalLines.get(i.intValue());
  157. p.setValue(value);
  158. } else {
  159. key = escapeName(key);
  160. Pair p = new Pair(key, value);
  161. p.setNew(true);
  162. keyedPairLines.put(key, new Integer(logicalLines.size()));
  163. logicalLines.add(p);
  164. }
  165. }
  166. public void clear() {
  167. super.clear();
  168. keyedPairLines.clear();
  169. logicalLines.clear();
  170. }
  171. public Object remove(Object key) {
  172. Object obj = super.remove(key);
  173. Integer i = (Integer) keyedPairLines.remove(key);
  174. if (null != i) {
  175. if (removeComments) {
  176. removeCommentsEndingAt(i.intValue());
  177. }
  178. logicalLines.set(i.intValue(), null);
  179. }
  180. return obj;
  181. }
  182. public Object clone() {
  183. LayoutPreservingProperties dolly = (LayoutPreservingProperties) super.clone();
  184. dolly.keyedPairLines = (HashMap) this.keyedPairLines.clone();
  185. dolly.logicalLines = (ArrayList) this.logicalLines.clone();
  186. for (int j = 0; j < dolly.logicalLines.size(); j++) {
  187. LogicalLine line = (LogicalLine) dolly.logicalLines.get(j);
  188. if (line instanceof Pair) {
  189. Pair p = (Pair) line;
  190. dolly.logicalLines.set(j, p.clone());
  191. }
  192. // no reason to clone other lines are they are immutable
  193. }
  194. return dolly;
  195. }
  196. /**
  197. * Echo the lines of the properties (including blanks and comments) to the
  198. * stream.
  199. * @param out the stream to write to
  200. */
  201. public void listLines(PrintStream out) {
  202. out.println("-- logical lines --");
  203. Iterator i = logicalLines.iterator();
  204. while (i.hasNext()) {
  205. LogicalLine line = (LogicalLine) i.next();
  206. if (line instanceof Blank) {
  207. out.println("blank: \"" + line + "\"");
  208. }
  209. else if (line instanceof Comment) {
  210. out.println("comment: \"" + line + "\"");
  211. }
  212. else if (line instanceof Pair) {
  213. out.println("pair: \"" + line + "\"");
  214. }
  215. }
  216. }
  217. /**
  218. * Save the properties to a file.
  219. * @param dest the file to write to
  220. */
  221. public void saveAs(File dest) throws IOException {
  222. FileOutputStream fos = new FileOutputStream(dest);
  223. store(fos, null);
  224. fos.close();
  225. }
  226. public void store(OutputStream out, String header) throws IOException {
  227. OutputStreamWriter osw = new OutputStreamWriter(out, "ISO-8859-1");
  228. if (header != null) {
  229. osw.write("#" + header + LS);
  230. }
  231. osw.write("#" + (new Date()).toString() + LS);
  232. boolean writtenSep = false;
  233. for (Iterator i = logicalLines.iterator();i.hasNext();) {
  234. LogicalLine line = (LogicalLine) i.next();
  235. if (line instanceof Pair) {
  236. if (((Pair)line).isNew()) {
  237. if (!writtenSep) {
  238. osw.write(LS);
  239. }
  240. }
  241. osw.write(line.toString() + LS);
  242. }
  243. else if (line != null) {
  244. osw.write(line.toString() + LS);
  245. }
  246. }
  247. osw.close();
  248. }
  249. /**
  250. * Reads a properties file into an internally maintained collection of logical
  251. * lines (possibly spanning physcial lines), which make up the comments, blank
  252. * lines and properties of the file.
  253. * @param is the stream from which to read the data
  254. */
  255. private String readLines(InputStream is) throws IOException {
  256. InputStreamReader isr = new InputStreamReader(is, "ISO-8859-1");
  257. BufferedReader br = new BufferedReader(isr);
  258. if (logicalLines.size() > 0) {
  259. // we add a blank line for spacing
  260. logicalLines.add(new Blank());
  261. }
  262. String s = br.readLine();
  263. boolean continuation = false;
  264. boolean comment = false;
  265. StringBuffer fileBuffer = new StringBuffer();
  266. StringBuffer logicalLineBuffer = new StringBuffer();
  267. while (s != null) {
  268. fileBuffer.append(s).append(LS);
  269. if (continuation) {
  270. // put in the line feed that was removed
  271. s = "\n" + s;
  272. } else {
  273. // could be a comment, if first non-whitespace is a # or !
  274. comment = s.matches("^( |\t|\f)*(#|!).*");
  275. }
  276. // continuation if not a comment and the line ends is an odd number of backslashes
  277. if (!comment) {
  278. continuation = requiresContinuation(s);
  279. }
  280. logicalLineBuffer.append(s);
  281. if (!continuation) {
  282. LogicalLine line = null;
  283. if (comment) {
  284. line = new Comment(logicalLineBuffer.toString());
  285. } else if (logicalLineBuffer.toString().trim().length() == 0) {
  286. line = new Blank();
  287. } else {
  288. line = new Pair(logicalLineBuffer.toString());
  289. String key = unescape(((Pair)line).getName());
  290. if (keyedPairLines.containsKey(key)) {
  291. // this key is already present, so we remove it and add
  292. // the new one
  293. remove(key);
  294. }
  295. keyedPairLines.put(key, new Integer(logicalLines.size()));
  296. }
  297. logicalLines.add(line);
  298. logicalLineBuffer.setLength(0);
  299. }
  300. s = br.readLine();
  301. }
  302. return fileBuffer.toString();
  303. }
  304. /**
  305. * Returns <code>true</code> if the line represented by <code>s</code> is to be continued
  306. * on the next line of the file, or <code>false</code> otherwise.
  307. * @param s the contents of the line to examine
  308. * @return <code>true</code> if the line is to be continued, <code>false</code> otherwise
  309. */
  310. private boolean requiresContinuation(String s) {
  311. char[] ca = s.toCharArray();
  312. int i = ca.length - 1;
  313. while (i > 0 && ca[i] == '\\') {
  314. i--;
  315. }
  316. // trailing backslashes
  317. int tb = ca.length - i - 1;
  318. return tb % 2 == 1;
  319. }
  320. /**
  321. * Unescape the string according to the rules for a Properites file, as laid out in
  322. * the docs for <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  323. * @param s the string to unescape (coming from the source file)
  324. * @return the unescaped string
  325. */
  326. private String unescape(String s) {
  327. /*
  328. * The following combinations are converted:
  329. * \n newline
  330. * \r carraige return
  331. * \f form feed
  332. * \t tab
  333. * \\ backslash
  334. * \u0000 unicode character
  335. * Any other slash is ignored, so
  336. * \b becomes 'b'.
  337. */
  338. char[] ch = new char[s.length() + 1];
  339. s.getChars(0, s.length(), ch, 0);
  340. ch[s.length()] = '\n';
  341. StringBuffer buffy = new StringBuffer(s.length());
  342. for (int i = 0; i < ch.length; i++) {
  343. char c = ch[i];
  344. if (c == '\n') {
  345. // we have hit out end-of-string marker
  346. break;
  347. }
  348. else if (c == '\\') {
  349. // possibly an escape sequence
  350. c = ch[++i];
  351. if (c == 'n')
  352. buffy.append('\n');
  353. else if (c == 'r')
  354. buffy.append('\r');
  355. else if (c == 'f')
  356. buffy.append('\f');
  357. else if (c == 't')
  358. buffy.append('\t');
  359. else if (c == 'u') {
  360. // handle unicode escapes
  361. c = unescapeUnicode(ch, i+1);
  362. i += 4;
  363. buffy.append(c);
  364. }
  365. else
  366. buffy.append(c);
  367. }
  368. else {
  369. buffy.append(c);
  370. }
  371. }
  372. return buffy.toString();
  373. }
  374. /**
  375. * Retrieve the unicode character whose code is listed at position <code>i</code>
  376. * in the character array <code>ch</code>.
  377. * @param ch the character array containing the unicode character code
  378. * @return the character extracted
  379. */
  380. private char unescapeUnicode(char[] ch, int i) {
  381. String s = new String(ch, i, 4);
  382. return (char) Integer.parseInt(s, 16);
  383. }
  384. /**
  385. * Escape the string <code>s</code> according to the rules in the docs for
  386. * <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  387. * @param s the string to escape
  388. * @return the escaped string
  389. */
  390. private String escapeValue(String s) {
  391. return escape(s, false);
  392. }
  393. /**
  394. * Escape the string <code>s</code> according to the rules in the docs for
  395. * <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  396. * This method escapes all the whitespace, not just the stuff at the beginning.
  397. * @param s the string to escape
  398. * @return the escaped string
  399. */
  400. private String escapeName(String s) {
  401. return escape(s, true);
  402. }
  403. /**
  404. * Escape the string <code>s</code> according to the rules in the docs for
  405. * <a href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  406. * @param s the string to escape
  407. * @param escapeAllSpaces if <code>true</code> the method escapes all the spaces,
  408. * if <code>false</code>, it escapes only the leading whitespace
  409. * @return the escaped string
  410. */
  411. private String escape(String s, boolean escapeAllSpaces) {
  412. if (s == null) {
  413. return null;
  414. }
  415. char[] ch = new char[s.length()];
  416. s.getChars(0, s.length(), ch, 0);
  417. String forEscaping = "\t\f\r\n\\:=#!";
  418. String escaped = "tfrn\\:=#!";
  419. StringBuffer buffy = new StringBuffer(s.length());
  420. boolean leadingSpace = true;
  421. for (int i = 0; i < ch.length; i++) {
  422. char c = ch[i];
  423. if (c == ' ') {
  424. if (escapeAllSpaces || leadingSpace) {
  425. buffy.append("\\");
  426. }
  427. } else {
  428. leadingSpace = false;
  429. }
  430. int p = forEscaping.indexOf(c);
  431. if (p != -1) {
  432. buffy.append("\\").append(escaped.substring(p,p+1));
  433. } else if (c < 0x0020 || c > 0x007e) {
  434. buffy.append(escapeUnicode(c));
  435. } else {
  436. buffy.append(c);
  437. }
  438. }
  439. return buffy.toString();
  440. }
  441. /**
  442. * Return the unicode escape sequence for a character, in the form \u005CuNNNN.
  443. * @param ch the character to encode
  444. * @return the unicode escape sequence
  445. */
  446. private String escapeUnicode(char ch) {
  447. StringBuffer buffy = new StringBuffer("\\u");
  448. String hex = Integer.toHexString((int)ch);
  449. buffy.append("0000".substring(4-hex.length()));
  450. buffy.append(hex);
  451. return buffy.toString();
  452. }
  453. /**
  454. * Remove the comments in the leading up the {@link logicalLines} list leading
  455. * up to line <code>pos</code>.
  456. * @param pos the line number to which the comments lead
  457. */
  458. private void removeCommentsEndingAt(int pos) {
  459. /* We want to remove comments preceding this position. Step back counting
  460. * blank lines (call this range B1) until we hit something non-blank. If
  461. * what we hit is not a comment, then exit. If what we hit is a comment,
  462. * then step back counting comment lines (call this range C1). Nullify
  463. * lines in C1 and B1.
  464. */
  465. int end = pos - 1;
  466. // step pos back until it hits something non-blank
  467. for (pos = end; pos > 0; pos--) {
  468. if (!(logicalLines.get(pos) instanceof Blank)) {
  469. break;
  470. }
  471. }
  472. // if the thing it hits is not a comment, then we have nothing to remove
  473. if (!(logicalLines.get(pos) instanceof Comment)) {
  474. return;
  475. }
  476. // step back until we hit the start of the comment
  477. for (; pos >= 0; pos--) {
  478. if (!(logicalLines.get(pos) instanceof Comment)) {
  479. break;
  480. }
  481. }
  482. // now we want to delete from pos+1 to end
  483. for (pos++ ;pos <= end; pos++) {
  484. logicalLines.set(pos, null);
  485. }
  486. }
  487. /**
  488. * A logical line of the properties input stream.
  489. */
  490. private static abstract class LogicalLine {
  491. private String text;
  492. public LogicalLine(String text) {
  493. this.text = text;
  494. }
  495. public void setText(String text) {
  496. this.text = text;
  497. }
  498. public String toString() {
  499. return text;
  500. }
  501. }
  502. /**
  503. * A blank line of the input stream.
  504. */
  505. private static class Blank extends LogicalLine {
  506. public Blank() {
  507. super("");
  508. }
  509. }
  510. /**
  511. * A comment line of the input stream.
  512. */
  513. private class Comment extends LogicalLine {
  514. public Comment(String text) {
  515. super(text);
  516. }
  517. }
  518. /**
  519. * A key-value pair from the input stream. This may span more than one physical
  520. * line, but it is constitutes as a single logical line.
  521. */
  522. private static class Pair extends LogicalLine implements Cloneable {
  523. private String name;
  524. private String value;
  525. private boolean added;
  526. public Pair(String text) {
  527. super(text);
  528. parsePair(text);
  529. }
  530. public Pair(String name, String value) {
  531. this(name + "=" + value);
  532. }
  533. public String getName() {
  534. return name;
  535. }
  536. public String getValue() {
  537. return value;
  538. }
  539. public void setValue(String value) {
  540. this.value = value;
  541. setText(name + "=" + value);
  542. }
  543. public boolean isNew() {
  544. return added;
  545. }
  546. public void setNew(boolean val) {
  547. added = val;
  548. }
  549. public Object clone() {
  550. Object dolly = null;
  551. try {
  552. dolly = super.clone();
  553. }
  554. catch (CloneNotSupportedException e) {
  555. // should be fine
  556. e.printStackTrace();
  557. }
  558. return dolly;
  559. }
  560. private void parsePair(String text) {
  561. // need to find first non-escaped '=', ':', '\t' or ' '.
  562. int pos = findFirstSeparator(text);
  563. if (pos == -1) {
  564. // trim leading whitespace only
  565. name = text;
  566. value = null;
  567. }
  568. else {
  569. name = text.substring(0, pos);
  570. value = text.substring(pos+1, text.length());
  571. }
  572. // trim leading whitespace only
  573. name = stripStart(name, " \t\f");
  574. }
  575. private String stripStart(String s, String chars) {
  576. if (s == null) {
  577. return null;
  578. }
  579. int i = 0;
  580. for (;i < s.length(); i++) {
  581. if (chars.indexOf(s.charAt(i)) == -1) {
  582. break;
  583. }
  584. }
  585. if (i == s.length()) {
  586. return "";
  587. }
  588. return s.substring(i);
  589. }
  590. private int findFirstSeparator(String s) {
  591. // Replace double backslashes with underscores so that they don't
  592. // confuse us looking for '\t' or '\=', for example, but they also
  593. // don't change the position of other characters
  594. s = s.replaceAll("\\\\\\\\", "__");
  595. // Replace single backslashes followed by separators, so we don't
  596. // pick them up
  597. s = s.replaceAll("\\\\=", "__");
  598. s = s.replaceAll("\\\\:", "__");
  599. s = s.replaceAll("\\\\ ", "__");
  600. s = s.replaceAll("\\\\t", "__");
  601. // Now only the unescaped separators are left
  602. return indexOfAny(s, " :=\t");
  603. }
  604. private int indexOfAny(String s, String chars) {
  605. if (s == null || chars == null) {
  606. return -1;
  607. }
  608. int p = s.length() + 1;
  609. for (int i = 0; i < chars.length(); i++) {
  610. int x = s.indexOf(chars.charAt(i));
  611. if (x != -1 && x < p) {
  612. p = x;
  613. }
  614. }
  615. if (p == s.length() + 1) {
  616. return -1;
  617. }
  618. return p;
  619. }
  620. }
  621. }