001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.spi.project.support.ant;
043:
044: import java.io.BufferedReader;
045: import java.io.BufferedWriter;
046: import java.io.IOException;
047: import java.io.InputStream;
048: import java.io.InputStreamReader;
049: import java.io.OutputStream;
050: import java.io.OutputStreamWriter;
051: import java.util.AbstractMap;
052: import java.util.AbstractSet;
053: import java.util.ArrayList;
054: import java.util.Arrays;
055: import java.util.HashMap;
056: import java.util.Iterator;
057: import java.util.LinkedList;
058: import java.util.List;
059: import java.util.ListIterator;
060: import java.util.Map;
061: import java.util.NoSuchElementException;
062: import java.util.Set;
063:
064: // XXX: consider adding getInitialComment() and setInitialComment() methods
065: // (useful e.g. for GeneratedFilesHelper)
066:
067: /**
068: * Similar to {@link java.util.Properties} but designed to retain additional
069: * information needed for safe hand-editing.
070: * Useful for various <samp>*.properties</samp> in a project:
071: * <ol>
072: * <li>Can associate comments with particular entries.
073: * <li>Order of entries preserved during modifications whenever possible.
074: * <li>VCS-friendly: lines which are not semantically modified are not textually modified.
075: * <li>Can automatically insert line breaks in new or modified values at positions
076: * that are likely to be semantically meaningful, e.g. between path components
077: * </ol>
078: * The file format (including encoding etc.) is compatible with the regular JRE implementation.
079: * Only (non-null) String is supported for keys and values.
080: * This class is not thread-safe; use only from a single thread, or use {@link java.util.Collections#synchronizedMap}.
081: * @author Jesse Glick, David Konecny
082: */
083: public final class EditableProperties extends
084: AbstractMap<String, String> implements Cloneable {
085:
086: /** List of Item instances as read from the properties file. Order is important.
087: * Saving properties will save then in this order. */
088: private final LinkedList<Item> items;
089:
090: /** Map of [property key, Item instance] for faster access. */
091: private final Map<String, Item> itemIndex;
092:
093: private final boolean alphabetize;
094:
095: private static final String keyValueSeparators = "=: \t\r\n\f";
096:
097: private static final String strictKeyValueSeparators = "=:";
098:
099: private static final String whiteSpaceChars = " \t\r\n\f";
100:
101: private static final String commentChars = "#!";
102:
103: private static final String INDENT = " ";
104:
105: // parse states:
106: private static final int WAITING_FOR_KEY_VALUE = 1;
107: private static final int READING_KEY_VALUE = 2;
108:
109: /**
110: * Creates empty instance whose items will not be alphabetized.
111: */
112: public EditableProperties() {
113: this (/* mentioned in #64174 - documented default */false);
114: }
115:
116: /**
117: * Creates empty instance.
118: * @param alphabetize alphabetize new items according to key or not
119: */
120: public EditableProperties(boolean alphabetize) {
121: this .alphabetize = alphabetize;
122: items = new LinkedList<Item>();
123: itemIndex = new HashMap<String, Item>();
124: }
125:
126: /**
127: * Creates instance from an existing map. No comments will be defined.
128: * Any order from the existing map will be retained,
129: * and further additions will not be alphabetized.
130: * @param map a map from String to String
131: */
132: public EditableProperties(Map<String, String> map) {
133: this (false);
134: putAll(map);
135: }
136:
137: /**
138: * Creates new instance from an existing one.
139: * @param ep an instance of EditableProperties
140: */
141: private EditableProperties(EditableProperties ep) {
142: // #64174: use a simple deep copy for speed
143: alphabetize = ep.alphabetize;
144: items = new LinkedList<Item>();
145: itemIndex = new HashMap<String, Item>(
146: ep.items.size() * 4 / 3 + 1);
147: for (Item _i : ep.items) {
148: Item i = (Item) _i.clone();
149: items.add(i);
150: itemIndex.put(i.getKey(), i);
151: }
152: }
153:
154: /**
155: * Returns a set view of the mappings ordered according to their file
156: * position. Each element in this set is a Map.Entry. See
157: * {@link AbstractMap#entrySet} for more details.
158: * @return set with Map.Entry instances.
159: */
160: public Set<Map.Entry<String, String>> entrySet() {
161: return new SetImpl(this );
162: }
163:
164: /**
165: * Load properties from a stream.
166: * @param stream an input stream
167: * @throws IOException if the contents are malformed or the stream could not be read
168: */
169: public void load(InputStream stream) throws IOException {
170: int state = WAITING_FOR_KEY_VALUE;
171: BufferedReader input = new BufferedReader(
172: new InputStreamReader(stream, "ISO-8859-1"));
173: List<String> tempList = new LinkedList<String>();
174: String line;
175: int commentLinesCount = 0;
176: // Read block of lines and create instance of Item for each.
177: // Separator is: either empty line or valid end of proeprty declaration
178: while (null != (line = input.readLine())) {
179: tempList.add(line);
180: boolean empty = isEmpty(line);
181: boolean comment = isComment(line);
182: if (state == WAITING_FOR_KEY_VALUE) {
183: if (empty) {
184: // empty line: create Item without any key
185: createNonKeyItem(tempList);
186: commentLinesCount = 0;
187: } else {
188: if (comment) {
189: commentLinesCount++;
190: } else {
191: state = READING_KEY_VALUE;
192: }
193: }
194: }
195: if (state == READING_KEY_VALUE && !isContinue(line)) {
196: // valid end of property declaration: create Item for it
197: createKeyItem(tempList, commentLinesCount);
198: state = WAITING_FOR_KEY_VALUE;
199: commentLinesCount = 0;
200: }
201: }
202: if (tempList.size() > 0) {
203: if (state == READING_KEY_VALUE) {
204: // value was not ended correctly? ignore.
205: createKeyItem(tempList, commentLinesCount);
206: } else {
207: createNonKeyItem(tempList);
208: }
209: }
210: }
211:
212: /**
213: * Store properties to a stream.
214: * @param stream an output stream
215: * @throws IOException if the stream could not be written to
216: */
217: public void store(OutputStream stream) throws IOException {
218: boolean previousLineWasEmpty = true;
219: BufferedWriter output = new BufferedWriter(
220: new OutputStreamWriter(stream, "ISO-8859-1"));
221: for (Item item : items) {
222: if (item.isSeparate() && !previousLineWasEmpty) {
223: output.newLine();
224: }
225: String line = null;
226: Iterator<String> it = item.getRawData().iterator();
227: while (it.hasNext()) {
228: line = it.next();
229: output.write(line);
230: output.newLine();
231: }
232: if (line != null) {
233: previousLineWasEmpty = isEmpty(line);
234: }
235: }
236: output.flush();
237: }
238:
239: @Override
240: public String put(String key, String value) {
241: if (key == null || value == null) {
242: throw new NullPointerException();
243: }
244: Item item = itemIndex.get(key);
245: String result = null;
246: if (item != null) {
247: result = item.getValue();
248: item.setValue(value);
249: } else {
250: item = new Item(key, value);
251: addItem(item, alphabetize);
252: }
253: return result;
254: }
255:
256: /**
257: * Convenience method to get a property as a string.
258: * Same as {@link #get}; only here because of pre-generic code.
259: * @param key a property name; cannot be null nor empty
260: * @return the property value, or null if it was not defined
261: */
262: public String getProperty(String key) {
263: return get(key);
264: }
265:
266: /**
267: * Convenience method to set a property.
268: * Same as {@link #put}; only here because of pre-generic code.
269: * @param key a property name; cannot be null nor empty
270: * @param value the desired value; cannot be null
271: * @return previous value of the property or null if there was not any
272: */
273: public String setProperty(String key, String value) {
274: return put(key, value);
275: }
276:
277: /**
278: * Sets a property to a value broken into segments for readability.
279: * Same behavior as {@link #setProperty(String,String)} with the difference that each item
280: * will be stored on its own line of text. {@link #getProperty} will simply concatenate
281: * all the items into one string, so generally separators
282: * (such as <samp>:</samp> for path-like properties) must be included in
283: * the items (for example, at the end of all but the last item).
284: * @param key a property name; cannot be null nor empty
285: * @param value the desired value; cannot be null; can be empty array
286: * @return previous value of the property or null if there was not any
287: */
288: public String setProperty(String key, String[] value) {
289: String result = get(key);
290: if (key == null || value == null) {
291: throw new NullPointerException();
292: }
293: List<String> valueList = Arrays.asList(value);
294: Item item = itemIndex.get(key);
295: if (item != null) {
296: item.setValue(valueList);
297: } else {
298: addItem(new Item(key, valueList), alphabetize);
299: }
300: return result;
301: }
302:
303: /**
304: * Returns comment associated with the property. The comment lines are
305: * returned as defined in properties file, that is comment delimiter is
306: * included. Comment for property is defined as: continuous block of lines
307: * starting with comment delimiter which are followed by property
308: * declaration (no empty line separator allowed).
309: * @param key a property name; cannot be null nor empty
310: * @return array of String lines as specified in properties file; comment
311: * delimiter character is included
312: */
313: public String[] getComment(String key) {
314: Item item = itemIndex.get(key);
315: if (item == null) {
316: return new String[0];
317: }
318: return item.getComment();
319: }
320:
321: /**
322: * Create comment for the property.
323: * <p>Note: if a comment includes non-ISO-8859-1 characters, they will be written
324: * to disk using Unicode escapes (and {@link #getComment} will interpret
325: * such escapes), but of course they will be unreadable for humans.
326: * @param key a property name; cannot be null nor empty
327: * @param comment lines of comment which will be written just above
328: * the property; no reformatting; comment lines must start with
329: * comment delimiter; cannot be null; cannot be emty array
330: * @param separate whether the comment should be separated from previous
331: * item by empty line
332: */
333: public void setComment(String key, String[] comment,
334: boolean separate) {
335: // XXX: check validity of comment parameter
336: Item item = itemIndex.get(key);
337: if (item == null) {
338: throw new IllegalArgumentException(
339: "Cannot set comment for non-existing property "
340: + key);
341: }
342: item.setComment(comment, separate);
343: }
344:
345: @Override
346: public Object clone() {
347: return cloneProperties();
348: }
349:
350: /**
351: * Create an exact copy of this properties object.
352: * @return a clone of this object
353: */
354: public EditableProperties cloneProperties() {
355: return new EditableProperties(this );
356: }
357:
358: // non-key item is block of empty lines/comment not associated with any property
359: private void createNonKeyItem(List<String> lines) {
360: // First check that previous item is not non-key item.
361: if (!items.isEmpty()) {
362: Item item = items.getLast();
363: if (item.getKey() == null) {
364: // it is non-key item: merge them
365: item.addCommentLines(lines);
366: lines.clear();
367: return;
368: }
369: }
370: // create new non-key item
371: Item item = new Item(lines);
372: addItem(item, false);
373: lines.clear();
374: }
375:
376: // opposite to non-key item: item with valid property declaration and
377: // perhaps some comment lines
378: private void createKeyItem(List<String> lines, int commentLinesCount) {
379: Item item = new Item(lines.subList(0, commentLinesCount), lines
380: .subList(commentLinesCount, lines.size()));
381: addItem(item, false);
382: lines.clear();
383: }
384:
385: private void addItem(Item item, boolean sort) {
386: String key = item.getKey();
387: if (sort) {
388: assert key != null;
389: ListIterator<Item> it = items.listIterator();
390: while (it.hasNext()) {
391: String k = it.next().getKey();
392: if (k != null && k.compareToIgnoreCase(key) > 0) {
393: it.previous();
394: it.add(item);
395: itemIndex.put(key, item);
396: return;
397: }
398: }
399: }
400: items.add(item);
401: if (key != null) {
402: itemIndex.put(key, item);
403: }
404: }
405:
406: private void removeItem(Item item) {
407: items.remove(item);
408: if (item.getKey() != null) {
409: itemIndex.remove(item.getKey());
410: }
411: }
412:
413: // does property declaration continue on next line?
414: private boolean isContinue(String line) {
415: int index = line.length() - 1;
416: int slashCount = 0;
417: while (index >= 0 && line.charAt(index) == '\\') {
418: slashCount++;
419: index--;
420: }
421: // if line ends with odd number of backslash then property definition
422: // continues on next line
423: return (slashCount % 2 != 0);
424: }
425:
426: // does line start with comment delimiter? (whitespaces are ignored)
427: private static boolean isComment(String line) {
428: line = trimLeft(line);
429: if (line.length() != 0
430: && commentChars.indexOf(line.charAt(0)) != -1) {
431: return true;
432: } else {
433: return false;
434: }
435: }
436:
437: // is line empty? (whitespaces are ignored)
438: private static boolean isEmpty(String line) {
439: return trimLeft(line).length() == 0;
440: }
441:
442: // remove all whitespaces from left
443: private static String trimLeft(String line) {
444: int start = 0;
445: while (start < line.length()) {
446: if (whiteSpaceChars.indexOf(line.charAt(start)) == -1) {
447: break;
448: }
449: start++;
450: }
451: return line.substring(start);
452: }
453:
454: /**
455: * Representation of one item read from properties file. It can be either
456: * valid property declaration with associated comment or chunk of empty
457: * lines or lines with comment which are not associated with any property.
458: */
459: private static class Item implements Cloneable {
460:
461: /** Lines of comment as read from properties file and as they will be
462: * written back to properties file. */
463: private List<String> commentLines;
464:
465: /** Lines with property name and value declaration as read from
466: * properties file and as they will be written back to properties file. */
467: private List<String> keyValueLines;
468:
469: /** Property key */
470: private String key;
471:
472: /** Property value */
473: private String value;
474:
475: /** Should this property be separated from previous one by at least
476: * one empty line. */
477: private boolean separate;
478:
479: // constructor only for cloning
480: private Item() {
481: }
482:
483: /**
484: * Create instance which does not have any key and value - just
485: * some empty or comment lines. This item is READ-ONLY.
486: */
487: public Item(List<String> commentLines) {
488: this .commentLines = new ArrayList<String>(commentLines);
489: }
490:
491: /**
492: * Create instance from the lines of comment and property declaration.
493: * Property name and value will be split.
494: */
495: public Item(List<String> commentLines,
496: List<String> keyValueLines) {
497: this .commentLines = new ArrayList<String>(commentLines);
498: this .keyValueLines = new ArrayList<String>(keyValueLines);
499: parse(keyValueLines);
500: }
501:
502: /**
503: * Create new instance with key and value.
504: */
505: public Item(String key, String value) {
506: this .key = key;
507: this .value = value;
508: }
509:
510: /**
511: * Create new instance with key and value.
512: */
513: public Item(String key, List<String> value) {
514: this .key = key;
515: setValue(value);
516: }
517:
518: // backdoor for merging non-key items
519: void addCommentLines(List<String> lines) {
520: assert key == null;
521: commentLines.addAll(lines);
522: }
523:
524: public String[] getComment() {
525: String[] res = new String[commentLines.size()];
526: for (int i = 0; i < res.length; i++) {
527: // #60249: the comment might have Unicode chars in escapes.
528: res[i] = decodeUnicode(commentLines.get(i));
529: }
530: return res;
531: }
532:
533: public void setComment(String[] commentLines, boolean separate) {
534: this .separate = separate;
535: this .commentLines = new ArrayList<String>(
536: commentLines.length);
537: for (int i = 0; i < commentLines.length; i++) {
538: // #60249 again - write only ISO-8859-1.
539: this .commentLines.add(encodeUnicode(commentLines[i]));
540: }
541: }
542:
543: public String getKey() {
544: return key;
545: }
546:
547: public String getValue() {
548: return value;
549: }
550:
551: public void setValue(String value) {
552: this .value = value;
553: keyValueLines = null;
554: }
555:
556: public void setValue(List<String> value) {
557: StringBuffer val = new StringBuffer();
558: List<String> l = new ArrayList<String>();
559: if (!value.isEmpty()) {
560: l.add(encode(key, true) + "=\\"); // NOI18N
561: Iterator<String> it = value.iterator();
562: while (it.hasNext()) {
563: String s = it.next();
564: val.append(s);
565: s = encode(s, false);
566: l
567: .add(it.hasNext() ? INDENT + s + '\\'
568: : INDENT + s); // NOI18N
569: }
570: } else {
571: // #45061: for no vals, use just "prop="
572: l.add(encode(key, true) + '='); // NOI18N
573: }
574: this .value = val.toString();
575: keyValueLines = l;
576: }
577:
578: public boolean isSeparate() {
579: return separate;
580: }
581:
582: /**
583: * Returns persistent image of this property.
584: */
585: public List<String> getRawData() {
586: List<String> l = new ArrayList<String>();
587: if (commentLines != null) {
588: l.addAll(commentLines);
589: }
590: if (keyValueLines == null) {
591: keyValueLines = new ArrayList<String>();
592: if (key != null && value != null) {
593: keyValueLines.add(encode(key, true) + "="
594: + encode(value, false));
595: }
596: }
597: l.addAll(keyValueLines);
598: return l;
599: }
600:
601: private void parse(List<String> keyValueLines) {
602: // merge lines into one:
603: String line = mergeLines(keyValueLines);
604: // split key and value
605: splitKeyValue(line);
606: }
607:
608: private String mergeLines(List<String> lines) {
609: String line = ""; // XXX use StringBuilder instead
610: Iterator<String> it = lines.iterator();
611: while (it.hasNext()) {
612: String l = trimLeft(it.next());
613: // if this is not the last line then remove last backslash
614: if (it.hasNext()) {
615: assert l.endsWith("\\") : lines;
616: l = l.substring(0, l.length() - 1);
617: }
618: line += l;
619: }
620: return line;
621: }
622:
623: private void splitKeyValue(String line) {
624: int separatorIndex = 0;
625: while (separatorIndex < line.length()) {
626: char ch = line.charAt(separatorIndex);
627: if (ch == '\\') {
628: // ignore next one character
629: separatorIndex++;
630: } else {
631: if (keyValueSeparators.indexOf(ch) != -1) {
632: break;
633: }
634: }
635: separatorIndex++;
636: }
637: key = decode(line.substring(0, separatorIndex));
638: line = trimLeft(line.substring(separatorIndex));
639: if (line.length() == 0) {
640: value = "";
641: return;
642: }
643: if (strictKeyValueSeparators.indexOf(line.charAt(0)) != -1) {
644: line = trimLeft(line.substring(1));
645: }
646: value = decode(line);
647: }
648:
649: private static String decode(String input) {
650: char ch;
651: int len = input.length();
652: StringBuffer output = new StringBuffer(len);
653: for (int x = 0; x < len; x++) {
654: ch = input.charAt(x);
655: if (ch != '\\') {
656: output.append(ch);
657: continue;
658: }
659: x++;
660: if (x == len) {
661: // backslash at the end? syntax error: ignore it
662: continue;
663: }
664: ch = input.charAt(x);
665: if (ch == 'u') {
666: if (x + 5 > len) {
667: // unicode character not finished? syntax error: ignore
668: output.append(input.substring(x - 1));
669: x += 4;
670: continue;
671: }
672: String val = input.substring(x + 1, x + 5);
673: try {
674: output.append((char) Integer.parseInt(val, 16));
675: } catch (NumberFormatException e) {
676: // #46234: handle gracefully
677: output.append(input.substring(x - 1, x + 5));
678: }
679: x += 4;
680: } else {
681: if (ch == 't')
682: ch = '\t';
683: else if (ch == 'r')
684: ch = '\r';
685: else if (ch == 'n')
686: ch = '\n';
687: else if (ch == 'f')
688: ch = '\f';
689: output.append(ch);
690: }
691: }
692: return output.toString();
693: }
694:
695: private static String encode(String input, boolean escapeSpace) {
696: int len = input.length();
697: StringBuffer output = new StringBuffer(len * 2);
698:
699: for (int x = 0; x < len; x++) {
700: char ch = input.charAt(x);
701: switch (ch) {
702: case ' ':
703: if (x == 0 || escapeSpace) {
704: output.append('\\');
705: }
706: output.append(' ');
707: break;
708: case '\\':
709: output.append("\\\\");
710: break;
711: case '\t':
712: output.append("\\t");
713: break;
714: case '\n':
715: output.append("\\n");
716: break;
717: case '\r':
718: output.append("\\r");
719: break;
720: case '\f':
721: output.append("\\f");
722: break;
723: default:
724: if ((ch < 0x0020) || (ch > 0x007e)) {
725: output.append("\\u");
726: String hex = Integer.toHexString(ch);
727: for (int i = 0; i < 4 - hex.length(); i++) {
728: output.append('0');
729: }
730: output.append(hex);
731: } else {
732: output.append(ch);
733: }
734: }
735: }
736: return output.toString();
737: }
738:
739: private static String decodeUnicode(String input) {
740: char ch;
741: int len = input.length();
742: StringBuffer output = new StringBuffer(len);
743: for (int x = 0; x < len; x++) {
744: ch = input.charAt(x);
745: if (ch != '\\') {
746: output.append(ch);
747: continue;
748: }
749: x++;
750: if (x == len) {
751: // backslash at the end? syntax error: ignore it
752: continue;
753: }
754: ch = input.charAt(x);
755: if (ch == 'u') {
756: if (x + 5 > len) {
757: // unicode character not finished? syntax error: ignore
758: output.append(input.substring(x - 1));
759: x += 4;
760: continue;
761: }
762: String val = input.substring(x + 1, x + 5);
763: try {
764: output.append((char) Integer.parseInt(val, 16));
765: } catch (NumberFormatException e) {
766: // #46234: handle gracefully
767: output.append(input.substring(x - 1, x + 5));
768: }
769: x += 4;
770: } else {
771: output.append(ch);
772: }
773: }
774: return output.toString();
775: }
776:
777: private static String encodeUnicode(String input) {
778: int len = input.length();
779: StringBuffer output = new StringBuffer(len * 2);
780: for (int x = 0; x < len; x++) {
781: char ch = input.charAt(x);
782: if ((ch < 0x0020) || (ch > 0x007e)) {
783: output.append("\\u"); // NOI18N
784: String hex = Integer.toHexString(ch);
785: for (int i = 0; i < 4 - hex.length(); i++) {
786: output.append('0');
787: }
788: output.append(hex);
789: } else {
790: output.append(ch);
791: }
792: }
793: return output.toString();
794: }
795:
796: @Override
797: public Object clone() {
798: Item item = new Item();
799: if (keyValueLines != null) {
800: item.keyValueLines = new ArrayList<String>(
801: keyValueLines);
802: }
803: if (commentLines != null) {
804: item.commentLines = new ArrayList<String>(commentLines);
805: }
806: item.key = key;
807: item.value = value;
808: item.separate = separate;
809: return item;
810: }
811:
812: }
813:
814: private static class SetImpl extends
815: AbstractSet<Map.Entry<String, String>> {
816:
817: private EditableProperties props;
818:
819: public SetImpl(EditableProperties props) {
820: this .props = props;
821: }
822:
823: public Iterator<Map.Entry<String, String>> iterator() {
824: return new IteratorImpl(props);
825: }
826:
827: public int size() {
828: return props.items.size();
829: }
830:
831: }
832:
833: private static class IteratorImpl implements
834: Iterator<Map.Entry<String, String>> {
835:
836: private final EditableProperties props;
837: private ListIterator<Item> delegate;
838:
839: public IteratorImpl(EditableProperties props) {
840: this .props = props;
841: delegate = props.items.listIterator();
842: }
843:
844: public boolean hasNext() {
845: return findNext() != null;
846: }
847:
848: public Map.Entry<String, String> next() {
849: Item item = findNext();
850: if (item == null) {
851: throw new NoSuchElementException();
852: }
853: delegate.next();
854: return new MapEntryImpl(item);
855: }
856:
857: public void remove() {
858: delegate.previous();
859: Item item = findNext();
860: if (item == null) {
861: throw new IllegalStateException();
862: }
863: int index = delegate.nextIndex();
864: props.items.remove(item);
865: props.itemIndex.remove(item.getKey());
866: delegate = props.items.listIterator(index);
867: }
868:
869: private Item findNext() {
870: while (delegate.hasNext()) {
871: Item item = delegate.next();
872: if (item.getKey() != null && item.getValue() != null) {
873: // Found one. Back up!
874: delegate.previous();
875: return item;
876: }
877: }
878: return null;
879: }
880:
881: }
882:
883: private static class MapEntryImpl implements
884: Map.Entry<String, String> {
885:
886: private Item item;
887:
888: public MapEntryImpl(Item item) {
889: this .item = item;
890: }
891:
892: public String getKey() {
893: return item.getKey();
894: }
895:
896: public String getValue() {
897: return item.getValue();
898: }
899:
900: public String setValue(String value) {
901: String result = item.getValue();
902: item.setValue(value);
903: return result;
904: }
905:
906: }
907:
908: }
|