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