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 26 kB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  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.io.PushbackReader;
  30. import java.util.ArrayList;
  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 it can
  40. * write out the properties in as close a form as possible to the input.</p>
  41. * <p>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 String LS = StringUtils.LINE_SEP;
  80. /**
  81. * Logical lines have escaping and line continuation taken care
  82. * of. Comments and blank lines are logical lines; they are not
  83. * removed.
  84. */
  85. private ArrayList logicalLines = new ArrayList();
  86. /**
  87. * Position in the <code>logicalLines</code> list, keyed by property name.
  88. */
  89. private HashMap keyedPairLines = new HashMap();
  90. /**
  91. * Flag to indicate that, when we remove a property from the file, we
  92. * also want to remove the comments that precede it.
  93. */
  94. private boolean removeComments;
  95. /**
  96. * Create a new, empty, Properties collection, with no defaults.
  97. */
  98. public LayoutPreservingProperties() {
  99. super();
  100. }
  101. /**
  102. * Create a new, empty, Properties collection, with the specified defaults.
  103. * @param defaults the default property values
  104. */
  105. public LayoutPreservingProperties(final Properties defaults) {
  106. super(defaults);
  107. }
  108. /**
  109. * Returns <code>true</code> if comments are removed along with
  110. * properties, or <code>false</code> otherwise. If
  111. * <code>true</code>, then when a property is removed, the comment
  112. * preceding it in the original file is removed also.
  113. * @return <code>true</code> if leading comments are removed when
  114. * a property is removed; <code>false</code> otherwise
  115. */
  116. public boolean isRemoveComments() {
  117. return removeComments;
  118. }
  119. /**
  120. * Sets the behaviour for comments accompanying properties that
  121. * are being removed. If <code>true</code>, then when a property
  122. * is removed, the comment preceding it in the original file is
  123. * removed also.
  124. * @param val <code>true</code> if leading comments are to be
  125. * removed when a property is removed; <code>false</code>
  126. * otherwise
  127. */
  128. public void setRemoveComments(final boolean val) {
  129. removeComments = val;
  130. }
  131. @Override
  132. public void load(final InputStream inStream) throws IOException {
  133. final String s = readLines(inStream);
  134. final byte[] ba = s.getBytes(ResourceUtils.ISO_8859_1);
  135. final ByteArrayInputStream bais = new ByteArrayInputStream(ba);
  136. super.load(bais);
  137. }
  138. @Override
  139. public Object put(final Object key, final Object value) throws NullPointerException {
  140. final Object obj = super.put(key, value);
  141. // the above call will have failed if key or value are null
  142. innerSetProperty(key.toString(), value.toString());
  143. return obj;
  144. }
  145. @Override
  146. public Object setProperty(final String key, final String value)
  147. throws NullPointerException {
  148. final Object obj = super.setProperty(key, value);
  149. // the above call will have failed if key or value are null
  150. innerSetProperty(key, value);
  151. return obj;
  152. }
  153. /**
  154. * Store a new key-value pair, or add a new one. The normal
  155. * functionality is taken care of by the superclass in the call to
  156. * {@link #setProperty}; this method takes care of this classes
  157. * extensions.
  158. * @param key the key of the property to be stored
  159. * @param value the value to be stored
  160. */
  161. private void innerSetProperty(String key, String value) {
  162. value = escapeValue(value);
  163. if (keyedPairLines.containsKey(key)) {
  164. final Integer i = (Integer) keyedPairLines.get(key);
  165. final Pair p = (Pair) logicalLines.get(i.intValue());
  166. p.setValue(value);
  167. } else {
  168. key = escapeName(key);
  169. final Pair p = new Pair(key, value);
  170. p.setNew(true);
  171. keyedPairLines.put(key, new Integer(logicalLines.size()));
  172. logicalLines.add(p);
  173. }
  174. }
  175. @Override
  176. public void clear() {
  177. super.clear();
  178. keyedPairLines.clear();
  179. logicalLines.clear();
  180. }
  181. @Override
  182. public Object remove(final Object key) {
  183. final Object obj = super.remove(key);
  184. final Integer i = (Integer) keyedPairLines.remove(key);
  185. if (null != i) {
  186. if (removeComments) {
  187. removeCommentsEndingAt(i.intValue());
  188. }
  189. logicalLines.set(i.intValue(), null);
  190. }
  191. return obj;
  192. }
  193. @Override
  194. public Object clone() {
  195. final LayoutPreservingProperties dolly =
  196. (LayoutPreservingProperties) super.clone();
  197. dolly.keyedPairLines = (HashMap) this.keyedPairLines.clone();
  198. dolly.logicalLines = (ArrayList) this.logicalLines.clone();
  199. final int size = dolly.logicalLines.size();
  200. for (int j = 0; j < size; j++) {
  201. final LogicalLine line = (LogicalLine) dolly.logicalLines.get(j);
  202. if (line instanceof Pair) {
  203. final Pair p = (Pair) line;
  204. dolly.logicalLines.set(j, p.clone());
  205. }
  206. // no reason to clone other lines are they are immutable
  207. }
  208. return dolly;
  209. }
  210. /**
  211. * Echo the lines of the properties (including blanks and comments) to the
  212. * stream.
  213. * @param out the stream to write to
  214. */
  215. public void listLines(final PrintStream out) {
  216. out.println("-- logical lines --");
  217. final Iterator i = logicalLines.iterator();
  218. while (i.hasNext()) {
  219. final LogicalLine line = (LogicalLine) i.next();
  220. if (line instanceof Blank) {
  221. out.println("blank: \"" + line + "\"");
  222. } else if (line instanceof Comment) {
  223. out.println("comment: \"" + line + "\"");
  224. } else if (line instanceof Pair) {
  225. out.println("pair: \"" + line + "\"");
  226. }
  227. }
  228. }
  229. /**
  230. * Save the properties to a file.
  231. * @param dest the file to write to
  232. */
  233. public void saveAs(final File dest) throws IOException {
  234. final FileOutputStream fos = new FileOutputStream(dest);
  235. store(fos, null);
  236. fos.close();
  237. }
  238. @Override
  239. public void store(final OutputStream out, final String header) throws IOException {
  240. final OutputStreamWriter osw = new OutputStreamWriter(out, ResourceUtils.ISO_8859_1);
  241. int skipLines = 0;
  242. final int totalLines = logicalLines.size();
  243. if (header != null) {
  244. osw.write("#" + header + LS);
  245. if (totalLines > 0
  246. && logicalLines.get(0) instanceof Comment
  247. && header.equals(logicalLines.get(0).toString().substring(1))) {
  248. skipLines = 1;
  249. }
  250. }
  251. // we may be updatiung a file written by this class, replace
  252. // the date comment instead of adding a new one and preserving
  253. // the one written last time
  254. if (totalLines > skipLines
  255. && logicalLines.get(skipLines) instanceof Comment) {
  256. try {
  257. DateUtils.parseDateFromHeader(logicalLines
  258. .get(skipLines)
  259. .toString().substring(1));
  260. skipLines++;
  261. } catch (final java.text.ParseException pe) {
  262. // not an existing date comment
  263. }
  264. }
  265. osw.write("#" + DateUtils.getDateForHeader() + LS);
  266. boolean writtenSep = false;
  267. for (final Iterator i = logicalLines.subList(skipLines, totalLines).iterator();
  268. i.hasNext();) {
  269. final LogicalLine line = (LogicalLine) i.next();
  270. if (line instanceof Pair) {
  271. if (((Pair)line).isNew()) {
  272. if (!writtenSep) {
  273. osw.write(LS);
  274. writtenSep = true;
  275. }
  276. }
  277. osw.write(line.toString() + LS);
  278. } else if (line != null) {
  279. osw.write(line.toString() + LS);
  280. }
  281. }
  282. osw.close();
  283. }
  284. /**
  285. * Reads a properties file into an internally maintained
  286. * collection of logical lines (possibly spanning physcial lines),
  287. * which make up the comments, blank lines and properties of the
  288. * file.
  289. * @param is the stream from which to read the data
  290. */
  291. private String readLines(final InputStream is) throws IOException {
  292. final InputStreamReader isr = new InputStreamReader(is, ResourceUtils.ISO_8859_1);
  293. final PushbackReader pbr = new PushbackReader(isr, 1);
  294. if (logicalLines.size() > 0) {
  295. // we add a blank line for spacing
  296. logicalLines.add(new Blank());
  297. }
  298. String s = readFirstLine(pbr);
  299. final BufferedReader br = new BufferedReader(pbr);
  300. boolean continuation = false;
  301. boolean comment = false;
  302. final StringBuffer fileBuffer = new StringBuffer();
  303. final StringBuffer logicalLineBuffer = new StringBuffer();
  304. while (s != null) {
  305. fileBuffer.append(s).append(LS);
  306. if (continuation) {
  307. // put in the line feed that was removed
  308. s = "\n" + s;
  309. } else {
  310. // could be a comment, if first non-whitespace is a # or !
  311. comment = s.matches("^( |\t|\f)*(#|!).*");
  312. }
  313. // continuation if not a comment and the line ends is an
  314. // odd number of backslashes
  315. if (!comment) {
  316. continuation = requiresContinuation(s);
  317. }
  318. logicalLineBuffer.append(s);
  319. if (!continuation) {
  320. LogicalLine line = null;
  321. if (comment) {
  322. line = new Comment(logicalLineBuffer.toString());
  323. } else if (logicalLineBuffer.toString().trim().length() == 0) {
  324. line = new Blank();
  325. } else {
  326. line = new Pair(logicalLineBuffer.toString());
  327. final String key = unescape(((Pair)line).getName());
  328. if (keyedPairLines.containsKey(key)) {
  329. // this key is already present, so we remove it and add
  330. // the new one
  331. remove(key);
  332. }
  333. keyedPairLines.put(key, new Integer(logicalLines.size()));
  334. }
  335. logicalLines.add(line);
  336. logicalLineBuffer.setLength(0);
  337. }
  338. s = br.readLine();
  339. }
  340. return fileBuffer.toString();
  341. }
  342. /**
  343. * Reads the first line and determines the EOL-style of the file
  344. * (relies on the style to be consistent, of course).
  345. *
  346. * <p>Sets LS as a side-effect.</p>
  347. *
  348. * @return the first line without any line separator, leaves the
  349. * reader positioned after the first line separator
  350. *
  351. * @since Ant 1.8.2
  352. */
  353. private String readFirstLine(final PushbackReader r) throws IOException {
  354. final StringBuffer sb = new StringBuffer(80);
  355. int ch = r.read();
  356. boolean hasCR = false;
  357. // when reaching EOF before the first EOL, assume native line
  358. // feeds
  359. LS = StringUtils.LINE_SEP;
  360. while (ch >= 0) {
  361. if (hasCR && ch != '\n') {
  362. // line feed is sole CR
  363. r.unread(ch);
  364. break;
  365. }
  366. if (ch == '\r') {
  367. LS = "\r";
  368. hasCR = true;
  369. } else if (ch == '\n') {
  370. LS = hasCR ? "\r\n" : "\n";
  371. break;
  372. } else {
  373. sb.append((char) ch);
  374. }
  375. ch = r.read();
  376. }
  377. return sb.toString();
  378. }
  379. /**
  380. * Returns <code>true</code> if the line represented by
  381. * <code>s</code> is to be continued on the next line of the file,
  382. * or <code>false</code> otherwise.
  383. * @param s the contents of the line to examine
  384. * @return <code>true</code> if the line is to be continued,
  385. * <code>false</code> otherwise
  386. */
  387. private boolean requiresContinuation(final String s) {
  388. final char[] ca = s.toCharArray();
  389. int i = ca.length - 1;
  390. while (i > 0 && ca[i] == '\\') {
  391. i--;
  392. }
  393. // trailing backslashes
  394. final int tb = ca.length - i - 1;
  395. return tb % 2 == 1;
  396. }
  397. /**
  398. * Unescape the string according to the rules for a Properites
  399. * file, as laid out in the docs for <a
  400. * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  401. * @param s the string to unescape (coming from the source file)
  402. * @return the unescaped string
  403. */
  404. private String unescape(final String s) {
  405. /*
  406. * The following combinations are converted:
  407. * \n newline
  408. * \r carraige return
  409. * \f form feed
  410. * \t tab
  411. * \\ backslash
  412. * \u0000 unicode character
  413. * Any other slash is ignored, so
  414. * \b becomes 'b'.
  415. */
  416. final char[] ch = new char[s.length() + 1];
  417. s.getChars(0, s.length(), ch, 0);
  418. ch[s.length()] = '\n';
  419. final StringBuffer buffy = new StringBuffer(s.length());
  420. for (int i = 0; i < ch.length; i++) {
  421. char c = ch[i];
  422. if (c == '\n') {
  423. // we have hit out end-of-string marker
  424. break;
  425. } else if (c == '\\') {
  426. // possibly an escape sequence
  427. c = ch[++i];
  428. if (c == 'n') {
  429. buffy.append('\n');
  430. } else if (c == 'r') {
  431. buffy.append('\r');
  432. } else if (c == 'f') {
  433. buffy.append('\f');
  434. } else if (c == 't') {
  435. buffy.append('\t');
  436. } else if (c == 'u') {
  437. // handle unicode escapes
  438. c = unescapeUnicode(ch, i+1);
  439. i += 4;
  440. buffy.append(c);
  441. } else {
  442. buffy.append(c);
  443. }
  444. } else {
  445. buffy.append(c);
  446. }
  447. }
  448. return buffy.toString();
  449. }
  450. /**
  451. * Retrieve the unicode character whose code is listed at position
  452. * <code>i</code> in the character array <code>ch</code>.
  453. * @param ch the character array containing the unicode character code
  454. * @return the character extracted
  455. */
  456. private char unescapeUnicode(final char[] ch, final int i) {
  457. final String s = new String(ch, i, 4);
  458. return (char) Integer.parseInt(s, 16);
  459. }
  460. /**
  461. * Escape the string <code>s</code> according to the rules in the
  462. * docs for <a
  463. * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  464. * @param s the string to escape
  465. * @return the escaped string
  466. */
  467. private String escapeValue(final String s) {
  468. return escape(s, false);
  469. }
  470. /**
  471. * Escape the string <code>s</code> according to the rules in the
  472. * docs for <a
  473. * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  474. * This method escapes all the whitespace, not just the stuff at
  475. * the beginning.
  476. * @param s the string to escape
  477. * @return the escaped string
  478. */
  479. private String escapeName(final String s) {
  480. return escape(s, true);
  481. }
  482. /**
  483. * Escape the string <code>s</code> according to the rules in the
  484. * docs for <a
  485. * href="http://java.sun.com/j2se/1.3/docs/api/java/util/Properties.html">java.util.Properties</a>.
  486. * @param s the string to escape
  487. * @param escapeAllSpaces if <code>true</code> the method escapes
  488. * all the spaces, if <code>false</code>, it escapes only the
  489. * leading whitespace
  490. * @return the escaped string
  491. */
  492. private String escape(final String s, final boolean escapeAllSpaces) {
  493. if (s == null) {
  494. return null;
  495. }
  496. final char[] ch = new char[s.length()];
  497. s.getChars(0, s.length(), ch, 0);
  498. final String forEscaping = "\t\f\r\n\\:=#!";
  499. final String escaped = "tfrn\\:=#!";
  500. final StringBuffer buffy = new StringBuffer(s.length());
  501. boolean leadingSpace = true;
  502. for (int i = 0; i < ch.length; i++) {
  503. final char c = ch[i];
  504. if (c == ' ') {
  505. if (escapeAllSpaces || leadingSpace) {
  506. buffy.append("\\");
  507. }
  508. } else {
  509. leadingSpace = false;
  510. }
  511. final int p = forEscaping.indexOf(c);
  512. if (p != -1) {
  513. buffy.append("\\").append(escaped.substring(p,p+1));
  514. } else if (c < 0x0020 || c > 0x007e) {
  515. buffy.append(escapeUnicode(c));
  516. } else {
  517. buffy.append(c);
  518. }
  519. }
  520. return buffy.toString();
  521. }
  522. /**
  523. * Return the unicode escape sequence for a character, in the form
  524. * \u005CuNNNN.
  525. * @param ch the character to encode
  526. * @return the unicode escape sequence
  527. */
  528. private String escapeUnicode(final char ch) {
  529. return "\\" + UnicodeUtil.EscapeUnicode(ch);
  530. }
  531. /**
  532. * Remove the comments in the leading up the {@link logicalLines}
  533. * list leading up to line <code>pos</code>.
  534. * @param pos the line number to which the comments lead
  535. */
  536. private void removeCommentsEndingAt(int pos) {
  537. /* We want to remove comments preceding this position. Step
  538. * back counting blank lines (call this range B1) until we hit
  539. * something non-blank. If what we hit is not a comment, then
  540. * exit. If what we hit is a comment, then step back counting
  541. * comment lines (call this range C1). Nullify lines in C1 and
  542. * B1.
  543. */
  544. final int end = pos - 1;
  545. // step pos back until it hits something non-blank
  546. for (pos = end; pos > 0; pos--) {
  547. if (!(logicalLines.get(pos) instanceof Blank)) {
  548. break;
  549. }
  550. }
  551. // if the thing it hits is not a comment, then we have nothing
  552. // to remove
  553. if (!(logicalLines.get(pos) instanceof Comment)) {
  554. return;
  555. }
  556. // step back until we hit the start of the comment
  557. for (; pos >= 0; pos--) {
  558. if (!(logicalLines.get(pos) instanceof Comment)) {
  559. break;
  560. }
  561. }
  562. // now we want to delete from pos+1 to end
  563. for (pos++; pos <= end; pos++) {
  564. logicalLines.set(pos, null);
  565. }
  566. }
  567. /**
  568. * A logical line of the properties input stream.
  569. */
  570. private abstract static class LogicalLine {
  571. private String text;
  572. public LogicalLine(final String text) {
  573. this.text = text;
  574. }
  575. public void setText(final String text) {
  576. this.text = text;
  577. }
  578. @Override
  579. public String toString() {
  580. return text;
  581. }
  582. }
  583. /**
  584. * A blank line of the input stream.
  585. */
  586. private static class Blank extends LogicalLine {
  587. public Blank() {
  588. super("");
  589. }
  590. }
  591. /**
  592. * A comment line of the input stream.
  593. */
  594. private class Comment extends LogicalLine {
  595. public Comment(final String text) {
  596. super(text);
  597. }
  598. }
  599. /**
  600. * A key-value pair from the input stream. This may span more than
  601. * one physical line, but it is constitutes as a single logical
  602. * line.
  603. */
  604. private static class Pair extends LogicalLine implements Cloneable {
  605. private String name;
  606. private String value;
  607. private boolean added;
  608. public Pair(final String text) {
  609. super(text);
  610. parsePair(text);
  611. }
  612. public Pair(final String name, final String value) {
  613. this(name + "=" + value);
  614. }
  615. public String getName() {
  616. return name;
  617. }
  618. public String getValue() {
  619. return value;
  620. }
  621. public void setValue(final String value) {
  622. this.value = value;
  623. setText(name + "=" + value);
  624. }
  625. public boolean isNew() {
  626. return added;
  627. }
  628. public void setNew(final boolean val) {
  629. added = val;
  630. }
  631. @Override
  632. public Object clone() {
  633. Object dolly = null;
  634. try {
  635. dolly = super.clone();
  636. } catch (final CloneNotSupportedException e) {
  637. // should be fine
  638. e.printStackTrace(); //NOSONAR
  639. }
  640. return dolly;
  641. }
  642. private void parsePair(final String text) {
  643. // need to find first non-escaped '=', ':', '\t' or ' '.
  644. final int pos = findFirstSeparator(text);
  645. if (pos == -1) {
  646. // trim leading whitespace only
  647. name = text;
  648. value = null;
  649. } else {
  650. name = text.substring(0, pos);
  651. value = text.substring(pos+1, text.length());
  652. }
  653. // trim leading whitespace only
  654. name = stripStart(name, " \t\f");
  655. }
  656. private String stripStart(final String s, final String chars) {
  657. if (s == null) {
  658. return null;
  659. }
  660. int i = 0;
  661. for (;i < s.length(); i++) {
  662. if (chars.indexOf(s.charAt(i)) == -1) {
  663. break;
  664. }
  665. }
  666. if (i == s.length()) {
  667. return "";
  668. }
  669. return s.substring(i);
  670. }
  671. private int findFirstSeparator(String s) {
  672. // Replace double backslashes with underscores so that they don't
  673. // confuse us looking for '\t' or '\=', for example, but they also
  674. // don't change the position of other characters
  675. s = s.replaceAll("\\\\\\\\", "__");
  676. // Replace single backslashes followed by separators, so we don't
  677. // pick them up
  678. s = s.replaceAll("\\\\=", "__");
  679. s = s.replaceAll("\\\\:", "__");
  680. s = s.replaceAll("\\\\ ", "__");
  681. s = s.replaceAll("\\\\t", "__");
  682. // Now only the unescaped separators are left
  683. return indexOfAny(s, " :=\t");
  684. }
  685. private int indexOfAny(final String s, final String chars) {
  686. if (s == null || chars == null) {
  687. return -1;
  688. }
  689. int p = s.length() + 1;
  690. for (int i = 0; i < chars.length(); i++) {
  691. final int x = s.indexOf(chars.charAt(i));
  692. if (x != -1 && x < p) {
  693. p = x;
  694. }
  695. }
  696. if (p == s.length() + 1) {
  697. return -1;
  698. }
  699. return p;
  700. }
  701. }
  702. }