1.1 --- a/openide.util/src/org/openide/util/EditableProperties.java Sun Aug 09 02:49:58 2009 +0400
1.2 +++ b/openide.util/src/org/openide/util/EditableProperties.java Wed Aug 12 02:55:48 2009 +0400
1.3 @@ -82,24 +82,35 @@
1.4 * @since org.openide.util 7.26
1.5 */
1.6 public final class EditableProperties extends AbstractMap<String,String> implements Cloneable {
1.7 +
1.8 + private static class State {
1.9 + /** whether multiple EP instances are currently linking to this */
1.10 + boolean shared;
1.11 + /** List of Item instances as read from the properties file. Order is important.
1.12 + * Saving properties will save then in this order. */
1.13 + final LinkedList<Item> items;
1.14 + /** Map of [property key, Item instance] for faster access. */
1.15 + final Map<String, Item> itemIndex;
1.16 + /** create fresh state */
1.17 + State() {
1.18 + items = new LinkedList<Item>();
1.19 + itemIndex = new HashMap<String,Item>();
1.20 + }
1.21 + /** duplicate state */
1.22 + State(State original) {
1.23 + items = new LinkedList<Item>();
1.24 + itemIndex = new HashMap<String,Item>(original.items.size() * 4 / 3 + 1);
1.25 + for (Item _i : original.items) {
1.26 + Item i = (Item) _i.clone();
1.27 + items.add(i);
1.28 + itemIndex.put(i.getKey(), i);
1.29 + }
1.30 + }
1.31 + }
1.32
1.33 - /** List of Item instances as read from the properties file. Order is important.
1.34 - * Saving properties will save then in this order. */
1.35 - private final LinkedList<Item> items;
1.36 -
1.37 - /** Map of [property key, Item instance] for faster access. */
1.38 - private final Map<String,Item> itemIndex;
1.39 -
1.40 + private State state;
1.41 private final boolean alphabetize;
1.42
1.43 - private static final String keyValueSeparators = "=: \t\r\n\f";
1.44 -
1.45 - private static final String strictKeyValueSeparators = "=:";
1.46 -
1.47 - private static final String whiteSpaceChars = " \t\r\n\f";
1.48 -
1.49 - private static final String commentChars = "#!";
1.50 -
1.51 private static final String INDENT = " ";
1.52
1.53 // parse states:
1.54 @@ -112,8 +123,7 @@
1.55 */
1.56 public EditableProperties(boolean alphabetize) {
1.57 this.alphabetize = alphabetize;
1.58 - items = new LinkedList<Item>();
1.59 - itemIndex = new HashMap<String,Item>();
1.60 + state = new State();
1.61 }
1.62
1.63 /**
1.64 @@ -123,12 +133,13 @@
1.65 private EditableProperties(EditableProperties ep) {
1.66 // #64174: use a simple deep copy for speed
1.67 alphabetize = ep.alphabetize;
1.68 - items = new LinkedList<Item>();
1.69 - itemIndex = new HashMap<String,Item>(ep.items.size() * 4 / 3 + 1);
1.70 - for (Item _i : ep.items) {
1.71 - Item i = (Item) _i.clone();
1.72 - items.add(i);
1.73 - itemIndex.put(i.getKey(), i);
1.74 + state = ep.state;
1.75 + state.shared = true;
1.76 + }
1.77 +
1.78 + private void writeOperation() {
1.79 + if (state.shared) {
1.80 + state = new State(state);
1.81 }
1.82 }
1.83
1.84 @@ -139,7 +150,7 @@
1.85 * @return set with Map.Entry instances.
1.86 */
1.87 public Set<Map.Entry<String,String>> entrySet() {
1.88 - return new SetImpl(this);
1.89 + return new SetImpl();
1.90 }
1.91
1.92 /**
1.93 @@ -148,7 +159,7 @@
1.94 * @throws IOException if the contents are malformed or the stream could not be read
1.95 */
1.96 public void load(InputStream stream) throws IOException {
1.97 - int state = WAITING_FOR_KEY_VALUE;
1.98 + int parseState = WAITING_FOR_KEY_VALUE;
1.99 BufferedReader input = new BufferedReader(new InputStreamReader(stream, "ISO-8859-1"));
1.100 List<String> tempList = new LinkedList<String>();
1.101 String line;
1.102 @@ -159,7 +170,7 @@
1.103 tempList.add(line);
1.104 boolean empty = isEmpty(line);
1.105 boolean comment = isComment(line);
1.106 - if (state == WAITING_FOR_KEY_VALUE) {
1.107 + if (parseState == WAITING_FOR_KEY_VALUE) {
1.108 if (empty) {
1.109 // empty line: create Item without any key
1.110 createNonKeyItem(tempList);
1.111 @@ -168,19 +179,19 @@
1.112 if (comment) {
1.113 commentLinesCount++;
1.114 } else {
1.115 - state = READING_KEY_VALUE;
1.116 + parseState = READING_KEY_VALUE;
1.117 }
1.118 }
1.119 }
1.120 - if (state == READING_KEY_VALUE && !isContinue(line)) {
1.121 + if (parseState == READING_KEY_VALUE && !isContinue(line)) {
1.122 // valid end of property declaration: create Item for it
1.123 createKeyItem(tempList, commentLinesCount);
1.124 - state = WAITING_FOR_KEY_VALUE;
1.125 + parseState = WAITING_FOR_KEY_VALUE;
1.126 commentLinesCount = 0;
1.127 }
1.128 }
1.129 if (tempList.size() > 0) {
1.130 - if (state == READING_KEY_VALUE) {
1.131 + if (parseState == READING_KEY_VALUE) {
1.132 // value was not ended correctly? ignore.
1.133 createKeyItem(tempList, commentLinesCount);
1.134 } else {
1.135 @@ -197,7 +208,7 @@
1.136 public void store(OutputStream stream) throws IOException {
1.137 boolean previousLineWasEmpty = true;
1.138 BufferedWriter output = new BufferedWriter(new OutputStreamWriter(stream, "ISO-8859-1"));
1.139 - for (Item item : items) {
1.140 + for (Item item : state.items) {
1.141 if (item.isSeparate() && !previousLineWasEmpty) {
1.142 output.newLine();
1.143 }
1.144 @@ -220,7 +231,7 @@
1.145 if (!(key instanceof String)) {
1.146 return null;
1.147 }
1.148 - Item item = itemIndex.get((String) key);
1.149 + Item item = state.itemIndex.get((String) key);
1.150 return item != null ? item.getValue() : null;
1.151 }
1.152
1.153 @@ -228,7 +239,8 @@
1.154 public String put(String key, String value) {
1.155 Parameters.notNull("key", key);
1.156 Parameters.notNull(key, value);
1.157 - Item item = itemIndex.get(key);
1.158 + writeOperation();
1.159 + Item item = state.itemIndex.get(key);
1.160 String result = null;
1.161 if (item != null) {
1.162 result = item.getValue();
1.163 @@ -278,7 +290,8 @@
1.164 throw new NullPointerException();
1.165 }
1.166 List<String> valueList = Arrays.asList(value);
1.167 - Item item = itemIndex.get(key);
1.168 + writeOperation();
1.169 + Item item = state.itemIndex.get(key);
1.170 if (item != null) {
1.171 item.setValue(valueList);
1.172 } else {
1.173 @@ -298,7 +311,7 @@
1.174 * delimiter character is included
1.175 */
1.176 public String[] getComment(String key) {
1.177 - Item item = itemIndex.get(key);
1.178 + Item item = state.itemIndex.get(key);
1.179 if (item == null) {
1.180 return new String[0];
1.181 }
1.182 @@ -319,7 +332,8 @@
1.183 */
1.184 public void setComment(String key, String[] comment, boolean separate) {
1.185 // XXX: check validity of comment parameter
1.186 - Item item = itemIndex.get(key);
1.187 + writeOperation();
1.188 + Item item = state.itemIndex.get(key);
1.189 if (item == null) {
1.190 throw new IllegalArgumentException("Cannot set comment for non-existing property "+key);
1.191 }
1.192 @@ -341,9 +355,10 @@
1.193
1.194 // non-key item is block of empty lines/comment not associated with any property
1.195 private void createNonKeyItem(List<String> lines) {
1.196 + writeOperation();
1.197 // First check that previous item is not non-key item.
1.198 - if (!items.isEmpty()) {
1.199 - Item item = items.getLast();
1.200 + if (!state.items.isEmpty()) {
1.201 + Item item = state.items.getLast();
1.202 if (item.getKey() == null) {
1.203 // it is non-key item: merge them
1.204 item.addCommentLines(lines);
1.205 @@ -366,23 +381,24 @@
1.206 }
1.207
1.208 private void addItem(Item item, boolean sort) {
1.209 + writeOperation();
1.210 String key = item.getKey();
1.211 if (sort) {
1.212 assert key != null;
1.213 - ListIterator<Item> it = items.listIterator();
1.214 + ListIterator<Item> it = state.items.listIterator();
1.215 while (it.hasNext()) {
1.216 String k = it.next().getKey();
1.217 if (k != null && k.compareToIgnoreCase(key) > 0) {
1.218 it.previous();
1.219 it.add(item);
1.220 - itemIndex.put(key, item);
1.221 + state.itemIndex.put(key, item);
1.222 return;
1.223 }
1.224 }
1.225 }
1.226 - items.add(item);
1.227 + state.items.add(item);
1.228 if (key != null) {
1.229 - itemIndex.put(key, item);
1.230 + state.itemIndex.put(key, item);
1.231 }
1.232 }
1.233
1.234 @@ -402,11 +418,14 @@
1.235 // does line start with comment delimiter? (whitespaces are ignored)
1.236 private static boolean isComment(String line) {
1.237 line = trimLeft(line);
1.238 - if (line.length() != 0 && commentChars.indexOf(line.charAt(0)) != -1) {
1.239 - return true;
1.240 - } else {
1.241 - return false;
1.242 + if (line.length() > 0) {
1.243 + switch (line.charAt(0)) {
1.244 + case '#':
1.245 + case '!':
1.246 + return true;
1.247 + }
1.248 }
1.249 + return false;
1.250 }
1.251
1.252 // is line empty? (whitespaces are ignored)
1.253 @@ -417,11 +436,19 @@
1.254 // remove all whitespaces from left
1.255 private static String trimLeft(String line) {
1.256 int start = 0;
1.257 - while (start < line.length()) {
1.258 - if (whiteSpaceChars.indexOf(line.charAt(start)) == -1) {
1.259 + int len = line.length();
1.260 + NONWS: while (start < len) {
1.261 + switch (line.charAt(start)) {
1.262 + case ' ':
1.263 + case '\t':
1.264 + case '\r':
1.265 + case '\n':
1.266 + case '\f':
1.267 + start++;
1.268 break;
1.269 + default:
1.270 + break NONWS;
1.271 }
1.272 - start++;
1.273 }
1.274 return line.substring(start);
1.275 }
1.276 @@ -575,7 +602,10 @@
1.277 splitKeyValue(line);
1.278 }
1.279
1.280 - private String mergeLines(List<String> lines) {
1.281 + private static String mergeLines(List<String> lines) {
1.282 + if (lines.size() == 1) {
1.283 + return trimLeft(lines.get(0));
1.284 + }
1.285 StringBuilder line = new StringBuilder();
1.286 Iterator<String> it = lines.iterator();
1.287 while (it.hasNext()) {
1.288 @@ -592,14 +622,22 @@
1.289
1.290 private void splitKeyValue(String line) {
1.291 int separatorIndex = 0;
1.292 - while (separatorIndex < line.length()) {
1.293 + int len = line.length();
1.294 + POS: while (separatorIndex < len) {
1.295 char ch = line.charAt(separatorIndex);
1.296 if (ch == '\\') {
1.297 // ignore next one character
1.298 separatorIndex++;
1.299 } else {
1.300 - if (keyValueSeparators.indexOf(ch) != -1) {
1.301 - break;
1.302 + switch (ch) {
1.303 + case '=':
1.304 + case ':':
1.305 + case ' ':
1.306 + case '\t':
1.307 + case '\r':
1.308 + case '\n':
1.309 + case '\f':
1.310 + break POS;
1.311 }
1.312 }
1.313 separatorIndex++;
1.314 @@ -610,13 +648,18 @@
1.315 value = "";
1.316 return;
1.317 }
1.318 - if (strictKeyValueSeparators.indexOf(line.charAt(0)) != -1) {
1.319 + switch (line.charAt(0)) {
1.320 + case '=':
1.321 + case ':':
1.322 line = trimLeft(line.substring(1));
1.323 }
1.324 value = decode(line);
1.325 }
1.326
1.327 private static String decode(String input) {
1.328 + if (input.indexOf('\\') == -1) {
1.329 + return input; // shortcut
1.330 + }
1.331 char ch;
1.332 int len = input.length();
1.333 StringBuilder output = new StringBuilder(len);
1.334 @@ -784,32 +827,26 @@
1.335
1.336 }
1.337
1.338 - private static class SetImpl extends AbstractSet<Map.Entry<String,String>> {
1.339 + private class SetImpl extends AbstractSet<Map.Entry<String,String>> {
1.340
1.341 - private EditableProperties props;
1.342 -
1.343 - public SetImpl(EditableProperties props) {
1.344 - this.props = props;
1.345 - }
1.346 + public SetImpl() {}
1.347
1.348 public Iterator<Map.Entry<String,String>> iterator() {
1.349 - return new IteratorImpl(props);
1.350 + return new IteratorImpl();
1.351 }
1.352
1.353 public int size() {
1.354 - return props.items.size();
1.355 + return state.items.size();
1.356 }
1.357
1.358 }
1.359
1.360 - private static class IteratorImpl implements Iterator<Map.Entry<String,String>> {
1.361 + private class IteratorImpl implements Iterator<Map.Entry<String,String>> {
1.362
1.363 - private final EditableProperties props;
1.364 private ListIterator<Item> delegate;
1.365
1.366 - public IteratorImpl(EditableProperties props) {
1.367 - this.props = props;
1.368 - delegate = props.items.listIterator();
1.369 + public IteratorImpl() {
1.370 + delegate = state.items.listIterator();
1.371 }
1.372
1.373 public boolean hasNext() {
1.374 @@ -832,9 +869,11 @@
1.375 throw new IllegalStateException();
1.376 }
1.377 int index = delegate.nextIndex();
1.378 - props.items.remove(item);
1.379 - props.itemIndex.remove(item.getKey());
1.380 - delegate = props.items.listIterator(index);
1.381 + writeOperation();
1.382 + Item removed = state.items.remove(index);
1.383 + assert removed.getKey().equals(item.getKey());
1.384 + state.itemIndex.remove(item.getKey());
1.385 + delegate = state.items.listIterator(index);
1.386 }
1.387
1.388 private Item findNext() {
1.389 @@ -851,7 +890,7 @@
1.390
1.391 }
1.392
1.393 - private static class MapEntryImpl implements Map.Entry<String,String> {
1.394 + private class MapEntryImpl implements Map.Entry<String,String> {
1.395
1.396 private Item item;
1.397
1.398 @@ -868,6 +907,8 @@
1.399 }
1.400
1.401 public String setValue(String value) {
1.402 + writeOperation();
1.403 + item = state.itemIndex.get(item.getKey());
1.404 String result = item.getValue();
1.405 item.setValue(value);
1.406 return result;
2.1 --- a/openide.util/test/unit/src/org/openide/util/EditablePropertiesTest.java Sun Aug 09 02:49:58 2009 +0400
2.2 +++ b/openide.util/test/unit/src/org/openide/util/EditablePropertiesTest.java Wed Aug 12 02:55:48 2009 +0400
2.3 @@ -135,6 +135,30 @@
2.4 assertFile("Saved cloned properties must be the same as original one", filenameOfTestProperties(), dest, (String)null);
2.5 }
2.6
2.7 + public void testCopyOnWriteClonability() throws Exception {
2.8 + EditableProperties ep1 = new EditableProperties(true);
2.9 + ep1.setProperty("k1", "v1");
2.10 + EditableProperties ep2 = ep1.cloneProperties();
2.11 + ep2.setProperty("k2", "v2");
2.12 + EditableProperties ep3 = ep2.cloneProperties();
2.13 + ep1.setProperty("k4", "v4");
2.14 + ep2.setProperty("k2", "v2a");
2.15 + Iterator<Map.Entry<String,String>> it = ep3.entrySet().iterator();
2.16 + it.next().setValue("v1b");
2.17 + it.next();
2.18 + it.remove();
2.19 + ep3.setProperty("k3", "v3");
2.20 + assertEquals("{k1=v1, k4=v4}", ep1.toString());
2.21 + assertEquals("{k1=v1, k2=v2a}", ep2.toString());
2.22 + assertEquals("{k1=v1b, k3=v3}", ep3.toString());
2.23 + ep1 = new EditableProperties(true);
2.24 + ep1.setProperty("k", "v1");
2.25 + ep2 = ep1.cloneProperties();
2.26 + ep2.entrySet().iterator().next().setValue("v2");
2.27 + assertEquals("{k=v1}", ep1.toString());
2.28 + assertEquals("{k=v2}", ep2.toString());
2.29 + }
2.30 +
2.31 // test that array values are stored correctly
2.32 public void testArrayValues() throws Exception {
2.33 EditableProperties ep = new EditableProperties(false);