Trying to speed up cloneProperties using copy-on-write semantics.
1.1 --- a/openide.util/src/org/openide/util/EditableProperties.java Mon Aug 10 14:36:37 2009 -0400
1.2 +++ b/openide.util/src/org/openide/util/EditableProperties.java Mon Aug 10 15:30:45 2009 -0400
1.3 @@ -82,14 +82,33 @@
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 INDENT = " ";
1.44 @@ -104,8 +123,7 @@
1.45 */
1.46 public EditableProperties(boolean alphabetize) {
1.47 this.alphabetize = alphabetize;
1.48 - items = new LinkedList<Item>();
1.49 - itemIndex = new HashMap<String,Item>();
1.50 + state = new State();
1.51 }
1.52
1.53 /**
1.54 @@ -115,12 +133,13 @@
1.55 private EditableProperties(EditableProperties ep) {
1.56 // #64174: use a simple deep copy for speed
1.57 alphabetize = ep.alphabetize;
1.58 - items = new LinkedList<Item>();
1.59 - itemIndex = new HashMap<String,Item>(ep.items.size() * 4 / 3 + 1);
1.60 - for (Item _i : ep.items) {
1.61 - Item i = (Item) _i.clone();
1.62 - items.add(i);
1.63 - itemIndex.put(i.getKey(), i);
1.64 + state = ep.state;
1.65 + state.shared = true;
1.66 + }
1.67 +
1.68 + private void writeOperation() {
1.69 + if (state.shared) {
1.70 + state = new State(state);
1.71 }
1.72 }
1.73
1.74 @@ -131,7 +150,7 @@
1.75 * @return set with Map.Entry instances.
1.76 */
1.77 public Set<Map.Entry<String,String>> entrySet() {
1.78 - return new SetImpl(this);
1.79 + return new SetImpl();
1.80 }
1.81
1.82 /**
1.83 @@ -140,7 +159,7 @@
1.84 * @throws IOException if the contents are malformed or the stream could not be read
1.85 */
1.86 public void load(InputStream stream) throws IOException {
1.87 - int state = WAITING_FOR_KEY_VALUE;
1.88 + int parseState = WAITING_FOR_KEY_VALUE;
1.89 BufferedReader input = new BufferedReader(new InputStreamReader(stream, "ISO-8859-1"));
1.90 List<String> tempList = new LinkedList<String>();
1.91 String line;
1.92 @@ -151,7 +170,7 @@
1.93 tempList.add(line);
1.94 boolean empty = isEmpty(line);
1.95 boolean comment = isComment(line);
1.96 - if (state == WAITING_FOR_KEY_VALUE) {
1.97 + if (parseState == WAITING_FOR_KEY_VALUE) {
1.98 if (empty) {
1.99 // empty line: create Item without any key
1.100 createNonKeyItem(tempList);
1.101 @@ -160,19 +179,19 @@
1.102 if (comment) {
1.103 commentLinesCount++;
1.104 } else {
1.105 - state = READING_KEY_VALUE;
1.106 + parseState = READING_KEY_VALUE;
1.107 }
1.108 }
1.109 }
1.110 - if (state == READING_KEY_VALUE && !isContinue(line)) {
1.111 + if (parseState == READING_KEY_VALUE && !isContinue(line)) {
1.112 // valid end of property declaration: create Item for it
1.113 createKeyItem(tempList, commentLinesCount);
1.114 - state = WAITING_FOR_KEY_VALUE;
1.115 + parseState = WAITING_FOR_KEY_VALUE;
1.116 commentLinesCount = 0;
1.117 }
1.118 }
1.119 if (tempList.size() > 0) {
1.120 - if (state == READING_KEY_VALUE) {
1.121 + if (parseState == READING_KEY_VALUE) {
1.122 // value was not ended correctly? ignore.
1.123 createKeyItem(tempList, commentLinesCount);
1.124 } else {
1.125 @@ -189,7 +208,7 @@
1.126 public void store(OutputStream stream) throws IOException {
1.127 boolean previousLineWasEmpty = true;
1.128 BufferedWriter output = new BufferedWriter(new OutputStreamWriter(stream, "ISO-8859-1"));
1.129 - for (Item item : items) {
1.130 + for (Item item : state.items) {
1.131 if (item.isSeparate() && !previousLineWasEmpty) {
1.132 output.newLine();
1.133 }
1.134 @@ -212,7 +231,7 @@
1.135 if (!(key instanceof String)) {
1.136 return null;
1.137 }
1.138 - Item item = itemIndex.get((String) key);
1.139 + Item item = state.itemIndex.get((String) key);
1.140 return item != null ? item.getValue() : null;
1.141 }
1.142
1.143 @@ -220,7 +239,8 @@
1.144 public String put(String key, String value) {
1.145 Parameters.notNull("key", key);
1.146 Parameters.notNull(key, value);
1.147 - Item item = itemIndex.get(key);
1.148 + writeOperation();
1.149 + Item item = state.itemIndex.get(key);
1.150 String result = null;
1.151 if (item != null) {
1.152 result = item.getValue();
1.153 @@ -270,7 +290,8 @@
1.154 throw new NullPointerException();
1.155 }
1.156 List<String> valueList = Arrays.asList(value);
1.157 - Item item = itemIndex.get(key);
1.158 + writeOperation();
1.159 + Item item = state.itemIndex.get(key);
1.160 if (item != null) {
1.161 item.setValue(valueList);
1.162 } else {
1.163 @@ -290,7 +311,7 @@
1.164 * delimiter character is included
1.165 */
1.166 public String[] getComment(String key) {
1.167 - Item item = itemIndex.get(key);
1.168 + Item item = state.itemIndex.get(key);
1.169 if (item == null) {
1.170 return new String[0];
1.171 }
1.172 @@ -311,7 +332,8 @@
1.173 */
1.174 public void setComment(String key, String[] comment, boolean separate) {
1.175 // XXX: check validity of comment parameter
1.176 - Item item = itemIndex.get(key);
1.177 + writeOperation();
1.178 + Item item = state.itemIndex.get(key);
1.179 if (item == null) {
1.180 throw new IllegalArgumentException("Cannot set comment for non-existing property "+key);
1.181 }
1.182 @@ -333,9 +355,10 @@
1.183
1.184 // non-key item is block of empty lines/comment not associated with any property
1.185 private void createNonKeyItem(List<String> lines) {
1.186 + writeOperation();
1.187 // First check that previous item is not non-key item.
1.188 - if (!items.isEmpty()) {
1.189 - Item item = items.getLast();
1.190 + if (!state.items.isEmpty()) {
1.191 + Item item = state.items.getLast();
1.192 if (item.getKey() == null) {
1.193 // it is non-key item: merge them
1.194 item.addCommentLines(lines);
1.195 @@ -358,23 +381,24 @@
1.196 }
1.197
1.198 private void addItem(Item item, boolean sort) {
1.199 + writeOperation();
1.200 String key = item.getKey();
1.201 if (sort) {
1.202 assert key != null;
1.203 - ListIterator<Item> it = items.listIterator();
1.204 + ListIterator<Item> it = state.items.listIterator();
1.205 while (it.hasNext()) {
1.206 String k = it.next().getKey();
1.207 if (k != null && k.compareToIgnoreCase(key) > 0) {
1.208 it.previous();
1.209 it.add(item);
1.210 - itemIndex.put(key, item);
1.211 + state.itemIndex.put(key, item);
1.212 return;
1.213 }
1.214 }
1.215 }
1.216 - items.add(item);
1.217 + state.items.add(item);
1.218 if (key != null) {
1.219 - itemIndex.put(key, item);
1.220 + state.itemIndex.put(key, item);
1.221 }
1.222 }
1.223
1.224 @@ -578,7 +602,7 @@
1.225 splitKeyValue(line);
1.226 }
1.227
1.228 - private String mergeLines(List<String> lines) {
1.229 + private static String mergeLines(List<String> lines) {
1.230 if (lines.size() == 1) {
1.231 return trimLeft(lines.get(0));
1.232 }
1.233 @@ -800,32 +824,26 @@
1.234
1.235 }
1.236
1.237 - private static class SetImpl extends AbstractSet<Map.Entry<String,String>> {
1.238 + private class SetImpl extends AbstractSet<Map.Entry<String,String>> {
1.239
1.240 - private EditableProperties props;
1.241 -
1.242 - public SetImpl(EditableProperties props) {
1.243 - this.props = props;
1.244 - }
1.245 + public SetImpl() {}
1.246
1.247 public Iterator<Map.Entry<String,String>> iterator() {
1.248 - return new IteratorImpl(props);
1.249 + return new IteratorImpl();
1.250 }
1.251
1.252 public int size() {
1.253 - return props.items.size();
1.254 + return state.items.size();
1.255 }
1.256
1.257 }
1.258
1.259 - private static class IteratorImpl implements Iterator<Map.Entry<String,String>> {
1.260 + private class IteratorImpl implements Iterator<Map.Entry<String,String>> {
1.261
1.262 - private final EditableProperties props;
1.263 private ListIterator<Item> delegate;
1.264
1.265 - public IteratorImpl(EditableProperties props) {
1.266 - this.props = props;
1.267 - delegate = props.items.listIterator();
1.268 + public IteratorImpl() {
1.269 + delegate = state.items.listIterator();
1.270 }
1.271
1.272 public boolean hasNext() {
1.273 @@ -848,9 +866,11 @@
1.274 throw new IllegalStateException();
1.275 }
1.276 int index = delegate.nextIndex();
1.277 - props.items.remove(item);
1.278 - props.itemIndex.remove(item.getKey());
1.279 - delegate = props.items.listIterator(index);
1.280 + writeOperation();
1.281 + Item removed = state.items.remove(index);
1.282 + assert removed.getKey().equals(item.getKey());
1.283 + state.itemIndex.remove(item.getKey());
1.284 + delegate = state.items.listIterator(index);
1.285 }
1.286
1.287 private Item findNext() {
1.288 @@ -867,7 +887,7 @@
1.289
1.290 }
1.291
1.292 - private static class MapEntryImpl implements Map.Entry<String,String> {
1.293 + private class MapEntryImpl implements Map.Entry<String,String> {
1.294
1.295 private Item item;
1.296
1.297 @@ -884,6 +904,8 @@
1.298 }
1.299
1.300 public String setValue(String value) {
1.301 + writeOperation();
1.302 + item = state.itemIndex.get(item.getKey());
1.303 String result = item.getValue();
1.304 item.setValue(value);
1.305 return result;
2.1 --- a/openide.util/test/unit/src/org/openide/util/EditablePropertiesTest.java Mon Aug 10 14:36:37 2009 -0400
2.2 +++ b/openide.util/test/unit/src/org/openide/util/EditablePropertiesTest.java Mon Aug 10 15:30:45 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);