[hg] main-silver: Issue #226111 - Public support for Preferences...

  • From: Svata Dedic < >
  • To:
  • Subject: [hg] main-silver: Issue #226111 - Public support for Preferences...
  • Date: Sun, 03 Mar 2013 02:39:50 -0800

changeset fab1458b9bec in main-silver ((none))
details: http://hg.netbeans.org/main-silver/rev/fab1458b9bec
description:
        Issue #226111 - Public support for Preferences editing + flush, 
support to detect override in Mime prefrences: fixed
        #226111: Implemented inheritable and transient Preferences

diffstat:

 editor.settings.storage/apichanges.xml                                       
                                                |    21 +
 editor.settings.storage/manifest.mf                                          
                                                |     2 +-
 
editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java
                          |   173 +
 
editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java
                        |    68 +
 
editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java
               |   258 ++
 
editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java
                    |    15 +-
 
editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java
               |  1124 ++++++++++
 
editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java
 |   544 ++++
 8 files changed, 2203 insertions(+), 2 deletions(-)

diffs (2273 lines):

diff --git a/editor.settings.storage/apichanges.xml 
b/editor.settings.storage/apichanges.xml
--- a/editor.settings.storage/apichanges.xml
+++ b/editor.settings.storage/apichanges.xml
@@ -108,6 +108,27 @@
 
     <changes>
         <change>
+            <summary>Detect preferences override and support 
inheritance</summary>
+            <version major="1" minor="38"/>
+            <date day="21" month="2" year="2013"/>
+            <author login="sdedic"/>
+            <compatibility addition="yes" binary="compatible" 
source="compatible" semantic="compatible"  deprecation="no" deletion="no" 
modification="no" />
+            <description>
+                <p>
+                    <code>OverridePreferences</code> can be used to detect 
whether the setting is defined for
+                    the specific MIME type, or inherited from default ('all 
languages') settings.
+                </p>
+                <p>
+                    During options editing, <code>MemoryPreferences</code> 
can be used to create Preferences 
+                    object, that propagates changes to its (persistent) 
delegate upon flush(). 
+                </p>
+            </description>
+            <class package="org.netbeans.editor.settings.storage.api" 
name="OverridePreferences"/>
+            <class package="org.netbeans.editor.settings.storage.api" 
name="MemoryPreferences"/>
+            <issue number="226111"/>
+        </change>
+        
+        <change>
             <summary>EditorSettings.PROP_MIME_TYPES</summary>
             <version major="1" minor="21"/>
             <date day="16" month="7" year="2009"/>
diff --git a/editor.settings.storage/manifest.mf 
b/editor.settings.storage/manifest.mf
--- a/editor.settings.storage/manifest.mf
+++ b/editor.settings.storage/manifest.mf
@@ -2,6 +2,6 @@
 OpenIDE-Module: org.netbeans.modules.editor.settings.storage/1
 OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/editor/settings/storage/Bundle.properties
 OpenIDE-Module-Provides: org.netbeans.api.editor.settings.implementation
-OpenIDE-Module-Specification-Version: 1.37
+OpenIDE-Module-Specification-Version: 1.38
 OpenIDE-Module-Layer: org/netbeans/modules/editor/settings/storage/layer.xml
 AutoUpdate-Show-In-Client: false
diff --git 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java
 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java
new file mode 100644
--- /dev/null
+++ 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/MemoryPreferences.java
@@ -0,0 +1,173 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License.  When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2013 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.editor.settings.storage.api;
+
+import java.util.prefs.Preferences;
+import 
org.netbeans.modules.editor.settings.storage.preferences.InheritedPreferences;
+import 
org.netbeans.modules.editor.settings.storage.preferences.ProxyPreferencesImpl;
+
+/**
+ * Preferences with a temporary storage, backed by another Preferences
+ * object. The instance tracks modifications done through the
+ * {@link Preferences} interface, but do not change the backing store
+ * until {@link Preferences#flush} is called.
+ * <p/>
+ * The MemoryPreferences object serves as an accessor, and offers some
+ * additional control for the Preferences tree. It should not be handed
+ * away, only the creator who manages the lifecycle should possess
+ * the MemoryPreferences instance. Other clients should be given just the
+ * Preferences object from {@link #getPreferences}.
+ * <p/>
+ * The returned Preferences object implements {@link LocalPreferences} 
extension
+ * interface.
+ * <p/>
+ * This implementation <b>does not support</b> sub-nodes.
+ * 
+ * @since 1.38
+ * 
+ * @author sdedic
+ * @author Vita Stejskal
+ */
+public final class MemoryPreferences  {
+    /**
+     * Returns an instance of Preferences backed by the delegate.
+     * A token is used to identify the desired Preferences set. As long as 
{@link #destroy} is not called,
+     * calls which use the same token & delegate will receive the same 
Preferences objects (though their
+     * MemoryPreferences may differ). The returned object implements {@link 
LocalPreferences}
+     * interface.
+     * 
+     * @param token token that determines the tree of Preferences.
+     * @param delegate
+     * @return MemoryPreferences accessor instance
+     */
+    public static MemoryPreferences get(Object token, Preferences delegate) {
+        return new 
MemoryPreferences(ProxyPreferencesImpl.getProxyPreferences(token, delegate));
+    }
+
+    /**
+     * Creates Preferences, which delegates to both persistent storage and 
parent (inherited) preferences.
+     * The persistent storage takes precedence over the parent. The {@link 
Preferences#remove} call is redefined
+     * for this case to just remove the key-value from the 'delegate', so 
that 'parent' value (if any) can become
+     * effective. Before {@link Preferences#flush}, the returned Preferences 
object delegates to both 'parent'
+     * and 'delegate' so that effective values can be seen. The returned 
object implements {@link LocalPreferences}
+     * interface.
+     * 
+     * @param token 
+     * @param parent
+     * @param delegate
+     * @return 
+     */
+    public static MemoryPreferences getWithInherited(Object token, 
Preferences parent, Preferences delegate) {
+        if (parent == null) {
+            return get(token, delegate);
+        }
+        InheritedPreferences inh = new InheritedPreferences(parent, 
delegate);
+        return new 
MemoryPreferences(ProxyPreferencesImpl.getProxyPreferences(token, inh));
+    }
+    
+    /**
+     * Provides the Preferences instance.
+     * The instance will collect writes in memory, as described in {@link 
MemoryPreferences} doc.
+     * 
+     * @return instance of Preferences
+     */
+    public Preferences  getPreferences() {
+        return prefInstance;
+    }
+    
+    /**
+     * Destroys the whole tree this Preferences belongs to.
+     * Individual Preferences node will not be flushed or cleared, but will 
become
+     * inaccessible from their token
+     * 
+     * @see {@link EditorSettings#getProxyPreferences}
+     */
+    public void destroy() {
+        prefInstance.destroy();
+    }
+    
+    /**
+     * Suppresses events from this Preferences node.
+     * During the Runnable execution, the Preferences node will not
+     * fire any events.
+     * 
+     * @param r runnable to execute
+     */
+    public void runWithoutEvents(Runnable r) {
+        try {
+            prefInstance.silence();
+            r.run();
+        } finally {
+            prefInstance.noise();
+        }
+    }
+    
+    /**
+     * Checks whether the Preferences node is changed. 
+     * Only value provided by the {@link #getPreferences} and values derived 
by call to {@link Preferences#node}
+     * on that instance are supported. In other words, Preferences object 
from the tree managed by this
+     * MemoryPreferences object. IllegalArgumentException can be thrown when 
an incompatible Preferences object
+     * is used.
+     * <p/>
+     * True will be returned, if the Preferences object is dirty and not 
flushed.
+     * 
+     * @param pref preferences node to check
+     * @return true, if the preferences was modified, and not flushed
+     * @throws IllegalArgumentException if the pref node is not from the 
managed tree.
+     */
+    public boolean isDirty(Preferences pref) {
+        if (!(pref instanceof ProxyPreferencesImpl)) {
+            throw new IllegalArgumentException("Incompatible 
PreferencesImpl");
+        }
+        ProxyPreferencesImpl impl = (ProxyPreferencesImpl)pref;
+        if (impl.node(prefInstance.absolutePath()) != prefInstance) {
+            throw new IllegalArgumentException("The preferences tree root is 
not reachable");
+        }
+        return impl.isDirty();
+    }
+
+    private ProxyPreferencesImpl prefInstance;
+    
+    private MemoryPreferences(ProxyPreferencesImpl delegate) {
+        this.prefInstance = delegate;
+    }
+}
diff --git 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java
 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java
new file mode 100644
--- /dev/null
+++ 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/api/OverridePreferences.java
@@ -0,0 +1,68 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License.  When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2013 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.editor.settings.storage.api;
+
+/**
+ * Mixin interface to detect if a value is inherited (defaulted) or not.
+ * The interface is to be implemented on Preferences objects (e.g. Mime 
Preferences),
+ * which support some sort of fallback, inheritance or default. It allows
+ * clients to determine whether a preference key is defined at the level 
represented
+ * by the Preferences object, or whether the value produced by {@link 
java.util.prefs.Preferences#get}
+ * originates in some form of default or inherited values.
+ * <p/>
+ * This interface is implemented on Editor settings Preferences objects 
+ * stored in MimeLookup (can be obtained by 
<code>MimeLookup.getLookup(mime).lookup(Preferences.class)</code>).
+ *
+ * @since 1.38
+ * @author sdedic
+ */
+public interface OverridePreferences {
+    /**
+     * Determines whether the value is defined locally.
+     * If the value comes from an inherited or default set of values,
+     * the method returns {@code false}.
+     * 
+     * @param key key to check
+     * @return true, if the value is defined locally, false if inherited.
+     */
+    public boolean      isOverriden(String key);
+}
diff --git 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java
 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java
new file mode 100644
--- /dev/null
+++ 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/InheritedPreferences.java
@@ -0,0 +1,258 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2013 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License.  When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2013 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.editor.settings.storage.preferences;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.prefs.AbstractPreferences;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.PreferenceChangeEvent;
+import java.util.prefs.PreferenceChangeListener;
+import java.util.prefs.Preferences;
+import org.netbeans.modules.editor.settings.storage.api.OverridePreferences;
+
+/**
+ * Support for inheriting Preferences, while still working with stored ones.
+ * 
+ * This class solves the 'diamond inheritance', which is present during 
editing:
+ * a MIME-type preferences derive from BOTH its persistent values 
(preferred) and
+ * from the parent, whose actual values are potentially transient, and also 
persistent.
+ * <p/>
+ * Let us assume the following assignment:
+ * <ul>
+ * <li>TC (this current) = currently added/changed/removed values
+ * <li>TP (this persistent) = persistent values, the getLocal() part of the 
Mime PreferencesImpl object
+ * <li>PC (parent current) = currently added/changed/removed values of the 
parent
+ * <li>PP (parent persistent) = persistent values, the getLocal() part of 
the parent MimePreferences
+ * </ul>
+ * The desired priority to find a value is: TC, TP, PC, PP. Because of 
{@link MemoryPreferences}, the
+ * PC, PP (and potentially fallback to a grandparent) we already have, if we 
use the parent's {@link MemoryPreferences}
+ * preferences as 'inherited'. The "TC" is handled by ProxyPreferences for 
this Mime node. In InheritedPreferences,
+ * we must only inject the TP in between TC and the parent's preferences 
(PC, PP, ...)
+ * <p/>
+ * The object is intended to act as a ProxyPreferences delegate, all writes 
go directly to the stored
+ * Mime preferences.
+ * 
+ * @author sdedic
+ */
+public final class InheritedPreferences extends AbstractPreferences 
implements PreferenceChangeListener, OverridePreferences  {
+    /**
+     * Preferences inherited, ie from a parent Mime type
+     */
+    private Preferences inherited;
+    
+    /**
+     * Stored preferences, 
+     */
+    private Preferences stored;
+    
+    public InheritedPreferences(Preferences inherited, Preferences stored) {
+        super(null, ""); // NOI18N
+        this.inherited = inherited;
+        if (!(stored instanceof OverridePreferences)) {
+            throw new IllegalArgumentException();
+        }
+        this.stored = stored;
+        
+        inherited.addPreferenceChangeListener(this);
+    }
+
+    /* package */ Preferences getParent() {
+        return inherited;
+    }
+
+    @Override
+    protected void putSpi(String key, String value) {
+        // do nothing, the AbstractPref then just fires an event
+    }
+
+    @Override
+    public void put(String key, String value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.put(key, value);
+        }
+        super.put(key, value);
+    }
+
+    @Override
+    public void putInt(String key, int value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putInt(key, value);
+        }
+        super.putInt(key, value); 
+    }
+
+    @Override
+    public void putLong(String key, long value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putLong(key, value);
+        }
+        super.putLong(key, value);
+    }
+
+    @Override
+    public void putBoolean(String key, boolean value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putBoolean(key, value);
+        }
+        super.putBoolean(key, value);
+    }
+
+    @Override
+    public void putFloat(String key, float value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putFloat(key, value);
+        }
+        super.putFloat(key, value); 
+    }
+
+    @Override
+    public void putDouble(String key, double value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putDouble(key, value);
+        }
+        super.putDouble(key, value); 
+    }
+
+    @Override
+    public void putByteArray(String key, byte[] value) {
+        if (Boolean.TRUE != ignorePut.get()) {
+            stored.putByteArray(key, value);
+        }
+        super.putByteArray(key, value);
+    }
+    
+    private ThreadLocal<Boolean> ignorePut = new ThreadLocal<Boolean>();
+
+    @Override
+    public void preferenceChange(PreferenceChangeEvent evt) {
+        if (!isOverriden(evt.getKey())) {
+            // jusr refires an event
+            ignorePut.set(true);
+            try {
+                put(evt.getKey(), evt.getNewValue());
+            } finally {
+                ignorePut.set(false);
+            }
+        }
+    }
+    
+    /**
+     * The value is defined locally, if the stored prefs define the value
+     * locally. The parent definitions do not count. It is expected, that the
+     * ProxyPreferences will report its local overrides as local in front of 
this
+     * InheritedPreferences.
+     * 
+     * @param k
+     * @return 
+     */
+    public @Override boolean isOverriden(String k) {
+        if (stored instanceof OverridePreferences) {
+            return ((OverridePreferences)stored).isOverriden(k);
+        } else {
+            return true;
+        }
+    }
+    
+    @Override
+    protected String getSpi(String key) {
+        // check the stored values
+        OverridePreferences localStored = (OverridePreferences)stored;
+        if (localStored.isOverriden(key)) {
+            return stored.get(key, null);
+        }
+        // fall back to the inherited prefs, potentially its stored values 
etc.
+        return inherited.get(key, null);
+    }
+
+    @Override
+    protected void removeSpi(String key) {
+        stored.remove(key);
+    }
+
+    @Override
+    protected void removeNodeSpi() throws BackingStoreException {
+        stored.removeNode();
+    }
+
+    @Override
+    protected String[] keysSpi() throws BackingStoreException {
+        Collection<String> names = new HashSet<String>();
+        names.addAll(Arrays.asList(stored.keys()));
+        names.addAll(Arrays.asList(inherited.keys()));
+        return names.toArray(new String[names.size()]);
+    }
+
+    @Override
+    protected String[] childrenNamesSpi() throws BackingStoreException {
+        if (stored != null) {
+            return stored.childrenNames();
+        } else {
+            return new String[0];
+        }
+    }
+
+    @Override
+    protected AbstractPreferences childSpi(String name) {
+        Preferences storedNode = stored != null ? stored.node(name) : null;
+        if (storedNode != null) {
+            return new InheritedPreferences(null, storedNode);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    protected void syncSpi() throws BackingStoreException {
+        stored.sync();
+    }
+
+    @Override
+    protected void flushSpi() throws BackingStoreException {
+        stored.flush();
+    }
+    
+    
+}
diff --git 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java
 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java
--- 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java
+++ 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/PreferencesImpl.java
@@ -63,6 +63,7 @@
 import java.util.prefs.Preferences;
 import org.netbeans.api.editor.mimelookup.MimePath;
 import 
org.netbeans.modules.editor.settings.storage.api.EditorSettingsStorage;
+import org.netbeans.modules.editor.settings.storage.api.OverridePreferences;
 import org.netbeans.modules.editor.settings.storage.spi.TypedValue;
 import org.openide.util.RequestProcessor;
 import org.openide.util.WeakListeners;
@@ -71,7 +72,7 @@
  *
  * @author vita
  */
-public final class PreferencesImpl extends AbstractPreferences implements 
PreferenceChangeListener {
+public final class PreferencesImpl extends AbstractPreferences implements 
PreferenceChangeListener, OverridePreferences {
 
     // the constant bellow is used in o.n.e.Settings!!
     private static final String JAVATYPE_KEY_PREFIX = 
"nbeditor-javaType-for-legacy-setting_"; //NOI18N
@@ -155,6 +156,18 @@
         }
     }
 
+    public @Override boolean isOverriden(String key) {
+        synchronized (lock) {
+            String bareKey;
+            if (key.startsWith(JAVATYPE_KEY_PREFIX)) {
+                bareKey = key.substring(JAVATYPE_KEY_PREFIX.length());
+            } else {
+                bareKey = key;
+            }
+            return getLocal().containsKey(bareKey);
+        }
+    }
+
     public @Override void remove(String key) {
         synchronized(lock) {
             String bareKey;
diff --git 
a/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java
 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java
new file mode 100644
--- /dev/null
+++ 
b/editor.settings.storage/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImpl.java
@@ -0,0 +1,1124 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2010 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License.  When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2008 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.modules.editor.settings.storage.preferences;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.NodeChangeEvent;
+import java.util.prefs.NodeChangeListener;
+import java.util.prefs.PreferenceChangeEvent;
+import java.util.prefs.PreferenceChangeListener;
+import java.util.prefs.Preferences;
+import javax.xml.bind.DatatypeConverter;
+import org.netbeans.modules.editor.settings.storage.api.OverridePreferences;
+import org.netbeans.modules.editor.settings.storage.spi.TypedValue;
+import org.openide.util.WeakListeners;
+
+/**
+ * Preferences impl that stores changes locally, and propagates them upon 
flush().
+ * The implementation is an adapted (former) implementation from 
org.netbeans.modules.options.indentation.ProxyPreferences.
+ * The original was moved here, and adapted to work with 'diamond' double 
defaulting: 1st default is
+ * the persistent Preferences object, where the changes will be finally 
propagated. The 2nd default
+ * is the Preferences object for the MIMEtype parent (if it exists). Keys 
that do not exist
+ * in the stored Preferences should delegate to the MIME parent. During 
editing, the MIME parent Preferences
+ * may get also changed, so we cannot rely on delegation between stored Mime 
Preferences, but must
+ * inject an additional path - see {@link InheritedPreferences}.
+ *
+ * @author sdedic
+ * @author vita
+ */
+public final class ProxyPreferencesImpl extends Preferences implements 
PreferenceChangeListener, NodeChangeListener, 
+        OverridePreferences {
+
+    /**
+     * Inherited preferences, for the case that key does not exist at our 
Node.
+     * Special handling for diamond inheritance. 
+     */
+    private Preferences inheritedPrefs;
+    
+    public static ProxyPreferencesImpl getProxyPreferences(Object token, 
Preferences delegate) {
+        return Tree.getTree(token, delegate).get(null, delegate.name(), 
delegate); //NOI18N
+    }
+    
+    public boolean isDirty() {
+        synchronized (tree.treeLock()) {
+            return !(data.isEmpty() && removedKeys.isEmpty() && 
children.isEmpty() && removedChildren.isEmpty()) || removed;
+        }
+    }
+
+    @Override
+    public void put(String key, String value) {
+        _put(key, value, String.class.getName());
+    }
+
+    @Override
+    public String get(String key, String def) {
+        synchronized (tree.treeLock()) {
+            checkNotNull(key, "key"); //NOI18N
+            checkRemoved();
+            
+            if (removedKeys.contains(key)) {
+                if (LOG.isLoggable(Level.FINE)) {
+                    LOG.fine("Key '" + key + "' removed, using default '" + 
def + "'"); //NOI18N
+                }
+                // removes will be flushed to the preferences, but now we 
need to see the defaults
+                // that WILL become effective after flush of this object.
+                if (inheritedPrefs != null) {
+                    return inheritedPrefs.get(key, def);
+                } else {
+                    return def;
+                }
+            } else {
+                TypedValue typedValue = data.get(key);
+                if (typedValue != null) {
+                    if (LOG.isLoggable(Level.FINE)) {
+                        LOG.fine("Key '" + key + "' modified, local value '" 
+ typedValue.getValue() + "'"); //NOI18N
+                    }
+                    return typedValue.getValue();
+                } else if (delegate != null) {
+                    String value = delegate.get(key, def);
+                    if (LOG.isLoggable(Level.FINE)) {
+                        LOG.fine("Key '" + key + "' undefined, original 
value '" + value + "'"); //NOI18N
+                    }
+                    return value;
+                } else {
+                    if (LOG.isLoggable(Level.FINE)) {
+                        LOG.fine("Key '" + key + "' undefined, '" + name + 
"' is a new node, using default '" + def + "'"); //NOI18N
+                    }
+                    return def;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void remove(String key) {
+        EventBag<PreferenceChangeListener, PreferenceChangeEvent> bag = null;
+        
+        synchronized (tree.treeLock()) {
+            checkNotNull(key, "key"); //NOI18N
+            checkRemoved();
+            
+            if (removedKeys.add(key)) {
+                data.remove(key);
+                bag = new EventBag<PreferenceChangeListener, 
PreferenceChangeEvent>();
+                bag.addListeners(prefListeners);
+                if (inheritedPrefs != null) {
+                    bag.addEvent(new PreferenceChangeEvent(this, key, 
+                            inheritedPrefs.get(key, null)));
+                } else {
+                    bag.addEvent(new PreferenceChangeEvent(this, key, null));
+                }
+            }
+        }
+
+        if (bag != null) {
+            firePrefEvents(Collections.singletonList(bag));
+        }
+    }
+
+    @Override
+    public void clear() throws BackingStoreException {
+        EventBag<PreferenceChangeListener, PreferenceChangeEvent> bag = new 
EventBag<PreferenceChangeListener, PreferenceChangeEvent>();
+        
+        synchronized (tree.treeLock()) {
+            checkRemoved();
+
+            // Determine modified or added keys
+            Set<String> keys = new HashSet<String>();
+            keys.addAll(data.keySet());
+            keys.removeAll(removedKeys);
+            if (!keys.isEmpty()) {
+                for(String key : keys) {
+                    String value = delegate == null ? null : 
delegate.get(key, null);
+                    bag.addEvent(new PreferenceChangeEvent(this, key, 
value));
+                }
+            }
+
+            // Determine removed keys
+            if (delegate != null) {
+                for(String key : removedKeys) {
+                    String value = delegate.get(key, null);
+                    if (value != null) {
+                        bag.addEvent(new PreferenceChangeEvent(this, key, 
value));
+                    }
+                }
+            }
+
+            // Initialize bag's listeners
+            bag.addListeners(prefListeners);
+
+            // Finally, remove the data
+            data.clear();
+            removedKeys.clear();
+        }
+        
+        firePrefEvents(Collections.singletonList(bag));
+    }
+
+    @Override
+    public void putInt(String key, int value) {
+        _put(key, Integer.toString(value), Integer.class.getName());
+    }
+
+    @Override
+    public int getInt(String key, int def) {
+        String value = get(key, null);
+        if (value != null) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException nfe) {
+                // ignore
+            }
+        }
+        return def;
+    }
+
+    @Override
+    public void putLong(String key, long value) {
+        _put(key, Long.toString(value), Long.class.getName());
+    }
+
+    @Override
+    public long getLong(String key, long def) {
+        String value = get(key, null);
+        if (value != null) {
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException nfe) {
+                // ignore
+            }
+        }
+        return def;
+    }
+
+    @Override
+    public void putBoolean(String key, boolean value) {
+        _put(key, Boolean.toString(value), Boolean.class.getName());
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean def) {
+        String value = get(key, null);
+        if (value != null) {
+            return Boolean.parseBoolean(value);
+        } else {
+            return def;
+        }
+    }
+
+    @Override
+    public void putFloat(String key, float value) {
+        _put(key, Float.toString(value), Float.class.getName());
+    }
+
+    @Override
+    public float getFloat(String key, float def) {
+        String value = get(key, null);
+        if (value != null) {
+            try {
+                return Float.parseFloat(value);
+            } catch (NumberFormatException nfe) {
+                // ignore
+            }
+        }
+        return def;
+    }
+
+    @Override
+    public void putDouble(String key, double value) {
+        _put(key, Double.toString(value), Double.class.getName());
+    }
+
+    @Override
+    public double getDouble(String key, double def) {
+        String value = get(key, null);
+        if (value != null) {
+            try {
+                return Double.parseDouble(value);
+            } catch (NumberFormatException nfe) {
+                // ignore
+            }
+        }
+        return def;
+    }
+
+    @Override
+    public void putByteArray(String key, byte[] value) {
+        _put(key, DatatypeConverter.printBase64Binary(value), 
value.getClass().getName());
+    }
+
+    @Override
+    public byte[] getByteArray(String key, byte[] def) {
+        String value = get(key, null);
+        if (value != null) {
+            byte [] decoded = DatatypeConverter.parseBase64Binary(value);
+            if (decoded != null) {
+                return decoded;
+            }
+        }
+        return def;
+    }
+
+    @Override
+    public String[] keys() throws BackingStoreException {
+        synchronized (tree.treeLock()) {
+            checkRemoved();
+            HashSet<String> keys = new HashSet<String>();
+            if (delegate != null) {
+                keys.addAll(Arrays.asList(delegate.keys()));
+            }
+            keys.addAll(data.keySet());
+            keys.removeAll(removedKeys);
+            return keys.toArray(new String [keys.size()]);
+        }
+    }
+
+    @Override
+    public String[] childrenNames() throws BackingStoreException {
+        synchronized (tree.treeLock()) {
+            checkRemoved();
+            HashSet<String> names = new HashSet<String>();
+            if (delegate != null) {
+                names.addAll(Arrays.asList(delegate.childrenNames()));
+            }
+            names.addAll(children.keySet());
+            names.removeAll(removedChildren);
+            return names.toArray(new String [names.size()]);
+        }
+    }
+
+    @Override
+    public Preferences parent() {
+        synchronized (tree.treeLock()) {
+            checkRemoved();
+            return parent;
+        }
+    }
+
+    @Override
+    public Preferences node(String pathName) {
+        Preferences node;
+        LinkedList<EventBag<NodeChangeListener, NodeChangeEvent>> events = 
new LinkedList<EventBag<NodeChangeListener, NodeChangeEvent>>();
+
+        synchronized (tree.treeLock()) {
+            checkNotNull(pathName, "pathName"); //NOI18N
+            checkRemoved();
+            node = node(pathName, true, events);
+        }
+
+        fireNodeEvents(events);
+        return node;
+    }
+
+    @Override
+    public boolean nodeExists(String pathName) throws BackingStoreException {
+        synchronized (tree.treeLock()) {
+            if (pathName.length() == 0) {
+                return !removed;
+            } else {
+                checkRemoved();
+                return node(pathName, false, null) != null;
+            }
+        }
+    }
+
+    @Override
+    public void removeNode() throws BackingStoreException {
+        synchronized (tree.treeLock()) {
+            checkRemoved();
+            ProxyPreferencesImpl p = parent;
+            if (p != null) {
+                p.removeChild(this);
+            } else {
+                throw new UnsupportedOperationException("Can't remove the 
root."); //NOI18N
+            }
+        }
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public String absolutePath() {
+        synchronized (tree.treeLock()) {
+            ProxyPreferencesImpl pp = parent;
+            if (pp != null) {
+                if (pp.parent == null) {
+                    // pp is the root, we don't want two consecutive slashes 
in the path
+                    return "/" + name(); //NOI18N
+                } else {
+                    return pp.absolutePath() + "/" + name(); //NOI18N
+                }
+            } else {
+                return "/"; //NOI18N
+            }
+        }
+    }
+
+    @Override
+    public boolean isUserNode() {
+        synchronized (tree.treeLock()) {
+            if (delegate != null) {
+                return delegate.isUserNode();
+            } else {
+                ProxyPreferencesImpl pp = parent;
+                if (pp != null) {
+                    return pp.isUserNode();
+                } else {
+                    return true;
+                }
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return (isUserNode() ? "User" : "System") + " Preference Node: " + 
absolutePath(); //NOI18N
+    }
+
+    @Override
+    public void flush() throws BackingStoreException {
+        synchronized (tree.treeLock()) {
+            if (LOG.isLoggable(Level.FINE)) {
+                LOG.fine("Flushing " + absolutePath());
+            }
+
+            checkRemoved();
+            for(ProxyPreferencesImpl pp : children.values()) {
+                pp.flush();
+            }
+
+            if (delegate == null) {
+                ProxyPreferencesImpl proxyRoot = parent.node("/", false, 
null); //NOI18N
+                assert proxyRoot != null : "Root must always exist"; //NOI18N
+
+                Preferences delegateRoot = proxyRoot.delegate;
+                assert delegateRoot != null : "Root must always have its 
corresponding delegate"; //NOI18N
+
+                Preferences nueDelegate = delegateRoot.node(absolutePath());
+                changeDelegate(nueDelegate);
+            }
+
+            delegate.removeNodeChangeListener(weakNodeListener);
+            delegate.removePreferenceChangeListener(weakPrefListener);
+            try {
+                // remove all removed children
+                for(String childName : removedChildren) {
+                    if (delegate.nodeExists(childName)) {
+                        delegate.node(childName).removeNode();
+                    }
+                }
+
+                // write all valid key-value pairs
+                for(String key : data.keySet()) {
+                    if (!removedKeys.contains(key)) {
+                        if (LOG.isLoggable(Level.FINE)) {
+                            LOG.fine("Writing " + absolutePath() + "/" + key 
+ "=" + data.get(key));
+                        }
+                        
+                        TypedValue typedValue = data.get(key);
+                        if 
(String.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.put(key, typedValue.getValue());
+
+                        } else if 
(Integer.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.putInt(key, 
Integer.parseInt(typedValue.getValue()));
+
+                        } else if 
(Long.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.putLong(key, 
Long.parseLong(typedValue.getValue()));
+
+                        } else if 
(Boolean.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.putBoolean(key, 
Boolean.parseBoolean(typedValue.getValue()));
+
+                        } else if 
(Float.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.putFloat(key, 
Float.parseFloat(typedValue.getValue()));
+
+                        } else if 
(Double.class.getName().equals(typedValue.getJavaType())) {
+                            delegate.putDouble(key, 
Double.parseDouble(typedValue.getValue()));
+
+                        } else {
+                            delegate.putByteArray(key, 
DatatypeConverter.parseBase64Binary(typedValue.getValue()));
+                        }
+                    }
+                }
+                data.clear();
+
+                // remove all removed keys
+                for(String key : removedKeys) {
+                    if (LOG.isLoggable(Level.FINE)) {
+                        LOG.fine("Removing " + absolutePath() + "/" + key);
+                    }
+                    delegate.remove(key);
+                }
+                removedKeys.clear();
+            } finally {
+                delegate.addNodeChangeListener(weakNodeListener);
+                delegate.addPreferenceChangeListener(weakPrefListener);
+            }
+        }        
+    }
+
+    @Override
+    public void sync() throws BackingStoreException {
+        ArrayList<EventBag<PreferenceChangeListener, PreferenceChangeEvent>> 
prefEvents = new ArrayList<EventBag<PreferenceChangeListener, 
PreferenceChangeEvent>>();
+        ArrayList<EventBag<NodeChangeListener, NodeChangeEvent>> nodeEvents 
= new ArrayList<EventBag<NodeChangeListener, NodeChangeEvent>>();
+
+        synchronized (tree.treeLock()) {
+            _sync(prefEvents, nodeEvents);
+        }
+
+        fireNodeEvents(nodeEvents);
+        firePrefEvents(prefEvents);
+    }
+
+    @Override
+    public void addPreferenceChangeListener(PreferenceChangeListener pcl) {
+        synchronized (tree.treeLock()) {
+            prefListeners.add(pcl);
+        }
+    }
+
+    @Override
+    public void removePreferenceChangeListener(PreferenceChangeListener pcl) 
{
+        synchronized (tree.treeLock()) {
+            prefListeners.remove(pcl);
+        }
+    }
+
+    @Override
+    public void addNodeChangeListener(NodeChangeListener ncl) {
+        synchronized (tree.treeLock()) {
+            nodeListeners.add(ncl);
+        }
+    }
+
+    @Override
+    public void removeNodeChangeListener(NodeChangeListener ncl) {
+        synchronized (tree.treeLock()) {
+            nodeListeners.remove(ncl);
+        }
+    }
+
+    @Override
+    public void exportNode(OutputStream os) throws IOException, 
BackingStoreException {
+        throw new UnsupportedOperationException("exportNode not supported");
+    }
+
+    @Override
+    public void exportSubtree(OutputStream os) throws IOException, 
BackingStoreException {
+        throw new UnsupportedOperationException("exportSubtree not 
supported");
+    }
+
+    // 
------------------------------------------------------------------------
+    // PreferenceChangeListener implementation
+    // 
------------------------------------------------------------------------
+
+    public void preferenceChange(PreferenceChangeEvent evt) {
+        PreferenceChangeListener [] listeners;
+        String nValue = evt.getNewValue();
+        String k = evt.getKey();
+        synchronized (tree.treeLock()) {
+            if (removed || data.containsKey(k)) {
+                return;
+            }
+            if (removedKeys.contains(k)) {
+                if (inheritedPrefs == null) {
+                    return;
+                } else {
+                    // if removed && there are inherited preferences, we 
must report the 'new value'
+                    // from the inherited prefs, as the override in our 
preferences is not in effect now.
+                    nValue = inheritedPrefs.get(k, null);
+                }
+            }
+            listeners = prefListeners.toArray(new 
PreferenceChangeListener[prefListeners.size()]);
+        }
+
+        PreferenceChangeEvent myEvt = null;
+        for(PreferenceChangeListener l : listeners) {
+            if (myEvt == null) {
+                myEvt = new PreferenceChangeEvent(this, k, nValue);
+            }
+            l.preferenceChange(myEvt);
+        }
+    }
+
+    // 
------------------------------------------------------------------------
+    // NodeChangeListener implementation
+    // 
------------------------------------------------------------------------
+
+    public void childAdded(NodeChangeEvent evt) {
+        NodeChangeListener [] listeners;
+        Preferences childNode;
+
+        synchronized (tree.treeLock()) {
+            String childName = evt.getChild().name();
+            if (removed || removedChildren.contains(childName)) {
+                return;
+            }
+
+            childNode = children.get(childName);
+            if (childNode != null) {
+                // swap delegates
+                ((ProxyPreferencesImpl) 
childNode).changeDelegate(evt.getChild());
+            } else {
+                childNode = node(evt.getChild().name());
+            }
+            
+            listeners = nodeListeners.toArray(new 
NodeChangeListener[nodeListeners.size()]);
+        }
+
+        NodeChangeEvent myEvt = null;
+        for(NodeChangeListener l : listeners) {
+            if (myEvt == null) {
+                myEvt = new NodeChangeEvent(this, childNode);
+            }
+            l.childAdded(evt);
+        }
+    }
+
+    public void childRemoved(NodeChangeEvent evt) {
+        NodeChangeListener [] listeners;
+        Preferences childNode;
+
+        synchronized (tree.treeLock()) {
+            String childName = evt.getChild().name();
+            if (removed || removedChildren.contains(childName)) {
+                return;
+            }
+
+            childNode = children.get(childName);
+            if (childNode != null) {
+                // swap delegates
+                ((ProxyPreferencesImpl) childNode).changeDelegate(null);
+            } else {
+                // nobody has accessed the child yet
+                return;
+            }
+            
+            listeners = nodeListeners.toArray(new 
NodeChangeListener[nodeListeners.size()]);
+        }
+
+        NodeChangeEvent myEvt = null;
+        for(NodeChangeListener l : listeners) {
+            if (myEvt == null) {
+                myEvt = new NodeChangeEvent(this, childNode);
+            }
+            l.childAdded(evt);
+        }
+    }
+    
+    // 
------------------------------------------------------------------------
+    // Other public implementation
+    // 
------------------------------------------------------------------------
+
+    /**
+     * Destroys whole preferences tree as if called on the root.
+     */
+    public void destroy() {
+        synchronized (tree.treeLock()) {
+            tree.destroy();
+        }
+    }
+    
+    public void silence() {
+        synchronized (tree.treeLock()) {
+            noEvents = true;
+        }
+    }
+    
+    public void noise() {
+        synchronized (tree.treeLock()) {
+            noEvents = false;
+        }
+    }
+
+    @Override
+    public boolean isOverriden(String key) {
+        return data.containsKey(key);
+    }
+    
+    // 
------------------------------------------------------------------------
+    // private implementation
+    // 
------------------------------------------------------------------------
+
+    private static final Logger LOG = 
Logger.getLogger(ProxyPreferencesImpl.class.getName());
+    
+    private final ProxyPreferencesImpl parent;
+    private final String name;
+    private Preferences delegate;
+    private final Tree tree;
+    private boolean removed;
+    
+    private final Map<String, TypedValue> data = new HashMap<String, 
TypedValue>();
+    private final Set<String> removedKeys = new HashSet<String>();
+    private final Map<String, ProxyPreferencesImpl> children = new 
HashMap<String, ProxyPreferencesImpl>();
+    private final Set<String> removedChildren = new HashSet<String>();
+
+    private boolean noEvents = false;
+    private PreferenceChangeListener weakPrefListener;
+    private final Set<PreferenceChangeListener> prefListeners = new 
HashSet<PreferenceChangeListener>();
+    private NodeChangeListener weakNodeListener;
+    private final Set<NodeChangeListener> nodeListeners = new 
HashSet<NodeChangeListener>();
+
+    private ProxyPreferencesImpl(ProxyPreferencesImpl parent, String name, 
Preferences delegate, Tree tree) {
+        assert name != null;
+        
+        this.parent = parent;
+        this.name = name;
+        this.delegate = delegate;
+        if (delegate instanceof InheritedPreferences) {
+            this.inheritedPrefs = 
((InheritedPreferences)delegate).getParent();
+        }
+        if (delegate != null) {
+            assert name.equals(delegate.name());
+
+            weakPrefListener = 
WeakListeners.create(PreferenceChangeListener.class, this, delegate);
+            delegate.addPreferenceChangeListener(weakPrefListener);
+            
+            weakNodeListener = 
WeakListeners.create(NodeChangeListener.class, this, delegate);
+            delegate.addNodeChangeListener(weakNodeListener);
+        }
+        this.tree = tree;
+    }
+
+    private void _put(String key, String value, String javaType) {
+        EventBag<PreferenceChangeListener, PreferenceChangeEvent> bag = null;
+
+        synchronized (tree.treeLock()) {
+            checkNotNull(key, "key"); //NOI18N
+            checkNotNull(value, "value"); //NOI18N
+            checkRemoved();
+            
+            String orig = get(key, null);
+            if (orig == null || !orig.equals(value)) {
+                if (LOG.isLoggable(Level.FINE)) {
+                    LOG.fine("Overwriting '" + key + "' = '" + value + "'"); 
//NOI18N
+                }
+                
+                data.put(key, new TypedValue(value, javaType));
+                removedKeys.remove(key);
+                
+                bag = new EventBag<PreferenceChangeListener, 
PreferenceChangeEvent>();
+                bag.addListeners(prefListeners);
+                bag.addEvent(new PreferenceChangeEvent(this, key, value));
+            }
+        }
+
+        if (bag != null) {
+            firePrefEvents(Collections.singletonList(bag));
+        }
+    }
+
+    private ProxyPreferencesImpl node(String pathName, boolean create, 
List<EventBag<NodeChangeListener, NodeChangeEvent>> events) {
+        if (pathName.length() > 0 && pathName.charAt(0) == '/') { //NOI18N
+            // absolute path, if this is not the root then find the root
+            // and pass the call to it
+            if (parent != null) {
+                Preferences root = this;
+                while (root.parent() != null) {
+                    root = root.parent();
+                }
+                return ((ProxyPreferencesImpl) root).node(pathName, create, 
events);
+            } else {
+                // this is the root, change the pathName to a relative path 
and proceed
+                pathName = pathName.substring(1);
+            }
+        }
+
+        if (pathName.length() > 0) {
+            String childName;
+            String pathFromChild;
+
+            int idx = pathName.indexOf('/'); //NOI18N
+            if (idx != -1) {
+                childName = pathName.substring(0, idx);
+                pathFromChild = pathName.substring(idx + 1);
+            } else {
+                childName = pathName;
+                pathFromChild = null;
+            }
+
+            ProxyPreferencesImpl child = children.get(childName);
+            if (child == null) {
+                if (removedChildren.contains(childName) && !create) {
+                    // this child has been removed
+                    return null;
+                }
+                
+                Preferences childDelegate = null;
+                try {
+                    if (delegate != null && delegate.nodeExists(childName)) {
+                        childDelegate = delegate.node(childName);
+                    }
+                } catch (BackingStoreException bse) {
+                    // ignore
+                }
+
+                if (childDelegate != null || create) {
+                    child = tree.get(this, childName, childDelegate);
+                    children.put(childName, child);
+                    removedChildren.remove(childName);
+
+                    // fire event if we really created the new child node
+                    if (childDelegate == null) {
+                        EventBag<NodeChangeListener, NodeChangeEvent> bag = 
new EventBag<NodeChangeListener, NodeChangeEvent>();
+                        bag.addListeners(nodeListeners);
+                        bag.addEvent(new NodeChangeEventExt(this, child, 
false));
+                        events.add(bag);
+                    }
+                } else {
+                    // childDelegate == null && !create
+                    return null;
+                }
+            } else {
+                assert !child.removed;
+            }
+
+            return pathFromChild != null ? child.node(pathFromChild, create, 
events) : child;
+        } else {
+            return this;
+        }
+    }
+
+    private void addChild(ProxyPreferencesImpl child) {
+        ProxyPreferencesImpl pp = children.get(child.name());
+        if (pp == null) {
+            children.put(child.name(), child);
+        } else {
+            assert pp == child;
+        }
+    }
+    
+    private void removeChild(ProxyPreferencesImpl child) {
+        assert child != null;
+        assert children.get(child.name()) == child;
+
+        child.nodeRemoved();
+        children.remove(child.name());
+        removedChildren.add(child.name());
+    }
+    
+    private void nodeRemoved() {
+        for(ProxyPreferencesImpl pp : children.values()) {
+            pp.nodeRemoved();
+        }
+
+        data.clear();
+        removedKeys.clear();
+        children.clear();
+        removedChildren.clear();
+        tree.removeNode(this);
+        
+        removed = true;
+    }
+    
+    private void checkNotNull(Object paramValue, String paramName) {
+        if (paramValue == null) {
+            throw new NullPointerException("The " + paramName + " must not 
be null");
+        }
+    }
+
+    private void checkRemoved() {
+        if (removed) {
+            throw new IllegalStateException("The node '" + this + " has 
already been removed."); //NOI18N
+        }
+    }
+
+    private void changeDelegate(Preferences nueDelegate) {
+        if (delegate != null) {
+            try {
+                if (delegate.nodeExists("")) { //NOI18N
+                    assert weakPrefListener != null;
+                    assert weakNodeListener != null;
+                    
delegate.removePreferenceChangeListener(weakPrefListener);
+                    delegate.removeNodeChangeListener(weakNodeListener);
+                }
+            } catch (BackingStoreException bse) {
+                LOG.log(Level.WARNING, null, bse);
+            }
+        }
+
+        delegate = nueDelegate;
+        weakPrefListener = null;
+        weakNodeListener = null;
+        
+        if (delegate != null) {
+            weakPrefListener = 
WeakListeners.create(PreferenceChangeListener.class, this, delegate);
+            delegate.addPreferenceChangeListener(weakPrefListener);
+            
+            weakNodeListener = 
WeakListeners.create(NodeChangeListener.class, this, delegate);
+            delegate.addNodeChangeListener(weakNodeListener);
+        }
+    }
+
+    private void _sync(
+        List<EventBag<PreferenceChangeListener, PreferenceChangeEvent>> 
prefEvents, 
+        List<EventBag<NodeChangeListener, NodeChangeEvent>> nodeEvents
+    ) {
+        // synchronize all children firts
+        for(ProxyPreferencesImpl pp : children.values()) {
+            pp._sync(prefEvents, nodeEvents);
+        }
+
+        // report all new children as removed
+        EventBag<NodeChangeListener, NodeChangeEvent> nodeBag = new 
EventBag<NodeChangeListener, NodeChangeEvent>();
+        nodeBag.addListeners(nodeListeners);
+
+        for(ProxyPreferencesImpl pp : children.values()) {
+            if (pp.delegate == null) {
+                // new node that does not have corresponding node in the 
original hierarchy
+                nodeBag.addEvent(new NodeChangeEventExt(this, pp, true));
+            }
+        }
+
+        if (!nodeBag.getEvents().isEmpty()) {
+            nodeEvents.add(nodeBag);
+        }
+
+        // report all modified keys
+        if (delegate != null) {
+            EventBag<PreferenceChangeListener, PreferenceChangeEvent> 
prefBag = new EventBag<PreferenceChangeListener, PreferenceChangeEvent>();
+            prefBag.addListeners(prefListeners);
+            prefEvents.add(prefBag);
+
+            for(String key : data.keySet()) {
+                prefBag.addEvent(new PreferenceChangeEvent(this, key, 
delegate.get(key, data.get(key).getValue())));
+            }
+        } // else there is no corresponding node in the orig hierarchy and 
this node
+          // will be reported as removed
+
+        // erase modified data
+        for(NodeChangeEvent nce : nodeBag.getEvents()) {
+            children.remove(nce.getChild().name());
+        }
+        data.clear();
+    }
+
+    private void firePrefEvents(List<EventBag<PreferenceChangeListener, 
PreferenceChangeEvent>> events) {
+        if (noEvents) {
+            return;
+        }
+        
+        for(EventBag<PreferenceChangeListener, PreferenceChangeEvent> bag : 
events) {
+            for(PreferenceChangeEvent event : bag.getEvents()) {
+                for(PreferenceChangeListener l : bag.getListeners()) {
+                    try {
+                        l.preferenceChange(event);
+                    } catch (Throwable t) {
+                        LOG.log(Level.WARNING, null, t);
+                    }
+                }
+            }
+        }
+    }
+
+    private void fireNodeEvents(List<EventBag<NodeChangeListener, 
NodeChangeEvent>> events) {
+        if (noEvents) {
+            return;
+        }
+        
+        for(EventBag<NodeChangeListener, NodeChangeEvent> bag : events) {
+            for(NodeChangeEvent event : bag.getEvents()) {
+                for(NodeChangeListener l : bag.getListeners()) {
+                    try {
+                        if ((event instanceof NodeChangeEventExt) && 
((NodeChangeEventExt) event).isRemovalEvent()) {
+                            l.childRemoved(event);
+                        } else {
+                            l.childAdded(event);
+                        }
+                    } catch (Throwable t) {
+                        LOG.log(Level.WARNING, null, t);
+                    }
+                }
+            }
+        }
+    }
+
+    /* test */ static final class Tree {
+
+        public static Tree getTree(Object token, Preferences prefs) {
+            synchronized (trees) {
+                // find all trees for the token
+                Map<Preferences, Tree> forest = trees.get(token);
+                if (forest == null) {
+                    forest = new HashMap<Preferences, Tree>();
+                    trees.put(token, forest);
+                }
+
+                // find the tree for the prefs' root
+                Preferences root = prefs.node("/"); //NOI18N
+                Tree tree = forest.get(root);
+                if (tree == null) {
+                    tree = new Tree(token, root);
+                    forest.put(root, tree);
+                }
+
+                return tree;
+            }
+        }
+
+        /* test */ static final Map<Object, Map<Preferences, Tree>> trees = 
new WeakHashMap<Object, Map<Preferences, Tree>>();
+
+        private final Preferences root;
+        private final Reference<?> tokenRef;
+        private final Map<String, ProxyPreferencesImpl> nodes = new 
HashMap<String, ProxyPreferencesImpl>();
+        
+        private Tree(Object token, Preferences root) {
+            this.root = root;
+            this.tokenRef = new WeakReference<Object>(token);
+        }
+
+        public Object treeLock() {
+            return this;
+        }
+
+        public ProxyPreferencesImpl get(ProxyPreferencesImpl parent, String 
name, Preferences delegate) {
+            if (delegate != null) {
+                assert name.equals(delegate.name());
+
+                if (parent == null) {
+                    Preferences parentDelegate = delegate.parent();
+                    if (parentDelegate != null) {
+                        parent = get(null, parentDelegate.name(), 
parentDelegate);
+                    } // else delegate is the root
+                } else {
+                    // sanity check
+                    assert parent.delegate == delegate.parent();
+                }
+            }
+
+            String absolutePath;
+            if (parent == null) {
+                absolutePath = "/"; //NOI18N
+            } else if (parent.parent() == null) {
+                absolutePath = "/" + name; //NOI18N
+            } else {
+                absolutePath = parent.absolutePath() + "/" + name; //NOI18N
+            }
+
+            ProxyPreferencesImpl node = nodes.get(absolutePath);
+            if (node == null) {
+                node = new ProxyPreferencesImpl(parent, name, delegate, 
this);
+                nodes.put(absolutePath, node);
+
+                if (parent != null) {
+                    parent.addChild(node);
+                }
+            } else {
+                assert !node.removed;
+            }
+
+            return node;
+        }
+
+        public void removeNode(ProxyPreferencesImpl node) {
+            String path = node.absolutePath();
+            assert nodes.containsKey(path);
+            ProxyPreferencesImpl pp = nodes.remove(path);
+        }
+
+        public void destroy() {
+            synchronized (trees) {
+                Object token = tokenRef.get();
+                if (token != null) {
+                    trees.remove(token);
+                } // else the token has been GCed and therefore is not even 
in the trees map
+            }
+        }
+    } // End of Tree class
+
+    private static final class EventBag<L, E extends EventObject> {
+        private final Set<L> listeners = new HashSet<L>();
+        private final Set<E> events = new HashSet<E>();
+
+        public EventBag() {
+        }
+
+        public Set<? extends L> getListeners() {
+            return listeners;
+        }
+
+        public Set<? extends E> getEvents() {
+            return events;
+        }
+
+        public void addListeners(Collection<? extends L> l) {
+            listeners.addAll(l);
+        }
+
+        public void addEvent(E event) {
+            events.add(event);
+        }
+    } // End of EventBag class
+
+    private static final class NodeChangeEventExt extends NodeChangeEvent {
+        private final boolean removal;
+        public NodeChangeEventExt(Preferences parent, Preferences child, 
boolean removal) {
+            super(parent, child);
+            this.removal = removal;
+        }
+
+        public boolean isRemovalEvent() {
+            return removal;
+        }
+    } // End of NodeChangeEventExt class
+}
diff --git 
a/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java
 
b/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java
new file mode 100644
--- /dev/null
+++ 
b/editor.settings.storage/test/unit/src/org/netbeans/modules/editor/settings/storage/preferences/ProxyPreferencesImplTest.java
@@ -0,0 +1,544 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2010 Oracle and/or its affiliates. All rights reserved.
+ *
+ * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
+ * Other names may be trademarks of their respective owners.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common
+ * Development and Distribution License("CDDL") (collectively, the
+ * "License"). You may not use this file except in compliance with the
+ * License. You can obtain a copy of the License at
+ * http://www.netbeans.org/cddl-gplv2.html
+ * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
+ * specific language governing permissions and limitations under the
+ * License.  When distributing the software, include this License Header
+ * Notice in each file and include the License file at
+ * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the GPL Version 2 section of the License file that
+ * accompanied this code. If applicable, add the following below the
+ * License Header, with the fields enclosed by brackets [] replaced by
+ * your own identifying information:
+ * "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * If you wish your version of this file to be governed by only the CDDL
+ * or only the GPL Version 2, indicate your decision by adding
+ * "[Contributor] elects to include this software in this distribution
+ * under the [CDDL or GPL Version 2] license." If you do not indicate a
+ * single choice of license, a recipient has the option to distribute
+ * your version of this file under either the CDDL, the GPL Version 2 or
+ * to extend the choice of license to its licensees as provided above.
+ * However, if you add GPL Version 2 code and therefore, elected the GPL
+ * Version 2 license, then the option applies only if the new code is
+ * made subject to such option by the copyright holder.
+ *
+ * Contributor(s):
+ *
+ * Portions Copyrighted 2008 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.modules.editor.settings.storage.preferences;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.AbstractPreferences;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+import static junit.framework.Assert.assertEquals;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.editor.settings.storage.api.OverridePreferences;
+import org.netbeans.modules.editor.settings.storage.api.MemoryPreferences;
+
+/**
+ *
+ * @author vita
+ */
+public class ProxyPreferencesImplTest extends NbTestCase {
+
+    public ProxyPreferencesImplTest(String name) {
+        super(name);
+    }
+
+    public void testSimpleRead() {
+        Preferences orig = Preferences.userRoot().node(getName());
+        orig.put("key-1", "value-1");
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        assertEquals("Wrong value", "value-1", test.get("key-1", null));
+    }
+    
+    public void testSimpleWrite() {
+        Preferences orig = Preferences.userRoot().node(getName());
+        assertNull("Original contains value", orig.get("key-1", null));
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        test.put("key-1", "xyz");
+        assertEquals("Wrong value", "xyz", test.get("key-1", null));
+    }
+
+    public void testBase64() {
+        Preferences orig = Preferences.userRoot().node(getName());
+        assertNull("Original contains value", orig.get("key-1", null));
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        test.putByteArray("key-1", "however you like it".getBytes());
+        assertEquals("Wrong value", "however you like it", new 
String(test.getByteArray("key-1", null)));
+    }
+    
+    public void testSimpleSync() throws BackingStoreException {
+        Preferences orig = Preferences.userRoot().node(getName());
+        assertNull("Original contains value", orig.get("key-1", null));
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        assertNull("Test should not contains pair", orig.get("key-1", null));
+
+        test.put("key-1", "xyz");
+        assertEquals("Test doesn't contain new pair", "xyz", 
test.get("key-1", null));
+
+        test.sync();
+        assertNull("Test didn't rollback pair", test.get("key-1", null));
+    }
+
+    public void testSimpleFlush() throws BackingStoreException {
+        Preferences orig = Preferences.userRoot().node(getName());
+        assertNull("Original contains value", orig.get("key-1", null));
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        assertNull("Test should not contains pair", orig.get("key-1", null));
+
+        test.put("key-1", "xyz");
+        assertEquals("Test doesn't contain new pair", "xyz", 
test.get("key-1", null));
+
+        test.flush();
+        assertEquals("Test should still contain the pair", "xyz", 
test.get("key-1", null));
+        assertEquals("Test didn't flush the pair", "xyz", orig.get("key-1", 
null));
+    }
+    
+    public void testSyncTree1() throws BackingStoreException {
+        String [] origTree = new String [] {
+            "CodeStyle/profile=GLOBAL",
+        };
+        String [] newTree = new String [] {
+            "CodeStyle/text/x-java/tab-size=2",
+            "CodeStyle/text/x-java/override-global-settings=true",
+            "CodeStyle/text/x-java/expand-tabs=true",
+            "CodeStyle/profile=PROJECT",
+        };
+
+        Preferences orig = Preferences.userRoot().node(getName());
+        write(orig, origTree);
+        checkContains(orig, origTree, "Orig");
+        checkNotContains(orig, newTree, "Orig");
+        
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        checkEquals("Test should be the same as Orig", orig, test);
+        
+        write(test, newTree);
+        checkContains(test, newTree, "Test");
+
+        test.sync();
+        checkContains(orig, origTree, "Orig");
+        checkNotContains(orig, newTree, "Orig");
+        checkContains(test, origTree, "Test");
+        checkNotContains(test, newTree, "Test");
+    }
+
+    public void testFlushTree1() throws BackingStoreException {
+        String [] origTree = new String [] {
+            "CodeStyle/profile=GLOBAL",
+        };
+        String [] newTree = new String [] {
+            "CodeStyle/text/x-java/tab-size=2",
+            "CodeStyle/text/x-java/override-global-settings=true",
+            "CodeStyle/text/x-java/expand-tabs=true",
+            "CodeStyle/profile=PROJECT",
+        };
+
+        Preferences orig = Preferences.userRoot().node(getName());
+        write(orig, origTree);
+        checkContains(orig, origTree, "Orig");
+        checkNotContains(orig, newTree, "Orig");
+        
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        checkEquals("Test should be the same as Orig", orig, test);
+        
+        write(test, newTree);
+        checkContains(test, newTree, "Test");
+
+        test.flush();
+        checkEquals("Test didn't flush to Orig", test, orig);
+    }
+
+    public void testRemoveKey() throws BackingStoreException {
+        Preferences orig = Preferences.userRoot().node(getName());
+        orig.put("key-2", "value-2");
+        assertNull("Original contains value", orig.get("key-1", null));
+        assertEquals("Original doesn't contain value", "value-2", 
orig.get("key-2", null));
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        test.put("key-1", "xyz");
+        assertEquals("Wrong value", "xyz", test.get("key-1", null));
+        
+        test.remove("key-1");
+        assertNull("Test contains removed key-1", test.get("key-1", null));
+        
+        test.remove("key-2");
+        assertNull("Test contains removed key-2", test.get("key-2", null));
+
+        test.flush();
+        assertNull("Test flushed removed key-1", orig.get("key-1", null));
+        assertNull("Test.flush did not remove removed key-2", 
orig.get("key-2", null));
+    }
+
+    public void testRemoveNode() throws BackingStoreException {
+        Preferences orig = Preferences.userRoot().node(getName());
+        Preferences origChild = orig.node("child");
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        assertTrue("Test child shoculd exist", test.nodeExists("child"));
+        Preferences testChild = test.node("child");
+
+        testChild.removeNode();
+        assertFalse("Removed test child should not exist", 
testChild.nodeExists(""));
+        assertFalse("Removed test child should not exist in parent", 
test.nodeExists("child"));
+
+        test.flush();
+        assertFalse("Test.flush did not remove orig child", 
origChild.nodeExists(""));
+        assertFalse("Test.flush did not remove orig child from parent", 
orig.nodeExists("child"));
+    }
+
+    public void testRemoveNodeCreateItAgain() throws BackingStoreException {
+        Preferences orig = Preferences.userRoot().node(getName());
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        Preferences testChild = test.node("child");
+
+        testChild.removeNode();
+        assertFalse("Removed test child should not exist", 
testChild.nodeExists(""));
+        assertFalse("Removed test child should not exist in parent", 
test.nodeExists("child"));
+
+        Preferences testChild2 = test.node("child");
+        assertTrue("Recreated test child should exist", 
testChild2.nodeExists(""));
+        assertTrue("Recreated test child should exist in parent", 
test.nodeExists("child"));
+        assertNotSame("Recreated child must not be the same as the removed 
one", testChild2, testChild);
+        assertEquals("Wrong childrenNames list", Arrays.asList(new String [] 
{ "child" }), Arrays.asList(test.childrenNames()));
+
+        try {
+            testChild.get("key", null);
+            fail("Removed test node should not be accessible");
+        } catch (Exception e) {
+        }
+
+        try {
+            testChild2.get("key", null);
+        } catch (Exception e) {
+            fail("Recreated test node should be accessible");
+        }
+
+    }
+
+    public void testRemoveHierarchy() throws BackingStoreException {
+        String [] origTree = new String [] {
+            "R.CodeStyle.project.expand-tabs=true",
+            "R.CodeStyle.project.indent-shift-width=6",
+            "R.CodeStyle.project.spaces-per-tab=6",
+            "R.CodeStyle.project.tab-size=7",
+            "R.CodeStyle.project.text-limit-width=88",
+            "R.CodeStyle.usedProfile=project",
+            "R.text.x-ruby.CodeStyle.project.indent-shift-width=2",
+            "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2",
+            "R.text.x-ruby.CodeStyle.project.tab-size=2",
+        };
+        String [] newTree = new String [] {
+            "R.CodeStyle.project.expand-tabs=true",
+            "R.CodeStyle.project.indent-shift-width=3",
+            "R.CodeStyle.project.spaces-per-tab=3",
+            "R.CodeStyle.project.tab-size=5",
+            "R.CodeStyle.project.text-limit-width=77",
+            "R.CodeStyle.usedProfile=project",
+            "R.text.x-ruby.CodeStyle.project.indent-shift-width=2",
+            "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2",
+            "R.text.x-ruby.CodeStyle.project.tab-size=2",
+        };
+
+        Preferences orig = Preferences.userRoot().node(getName());
+        write(orig, origTree);
+
+        checkContains(orig, origTree, "Orig");
+
+        Preferences test = ProxyPreferencesImpl.getProxyPreferences(this, 
orig);
+        checkEquals("Test should be the same as Orig", orig, test);
+
+        Preferences testRoot = test.node("R");
+        removeAllKidsAndKeys(testRoot);
+        
+        write(test, newTree);
+        checkContains(test, newTree, "Test");
+
+        test.flush();
+        checkEquals("Test didn't flush to Orig", test, orig);
+    }
+
+    public void testTreeGCed() throws BackingStoreException {
+        String [] newTree = new String [] {
+            "R.CodeStyle.project.expand-tabs=true",
+            "R.CodeStyle.project.indent-shift-width=3",
+            "R.CodeStyle.project.spaces-per-tab=3",
+            "R.CodeStyle.project.tab-size=5",
+            "R.CodeStyle.project.text-limit-width=77",
+            "R.CodeStyle.usedProfile=project",
+            "R.text.x-ruby.CodeStyle.project.indent-shift-width=2",
+            "R.text.x-ruby.CodeStyle.project.spaces-per-tab=2",
+            "R.text.x-ruby.CodeStyle.project.tab-size=2",
+        };
+
+        Preferences orig = Preferences.userRoot().node(getName());
+
+        Object treeToken = new Object();
+        Preferences test = 
ProxyPreferencesImpl.getProxyPreferences(treeToken, orig);
+        write(test, newTree);
+        checkContains(test, newTree, "Test");
+
+        Reference<Object> treeTokenRef = new 
WeakReference<Object>(treeToken);
+        Reference<Preferences> testRef = new 
WeakReference<Preferences>(test);
+        treeToken = null;
+        test = null;
+        assertGC("Tree token was not GCed", treeTokenRef, 
Collections.singleton(this));
+        // touch the WeakHashMap to expungeStaleEntries
+        Object dummyToken = new Object();
+        ProxyPreferencesImpl dummyPrefs = 
ProxyPreferencesImpl.getProxyPreferences(dummyToken, orig);
+        assertGC("Test preferences were not GCed", testRef, 
Collections.singleton(this));
+        
+    }
+    
+    /**
+     * Checks that a value not defined in delegate can be read from the 
parent prefs.
+     * Checks that if the parent prefs also do not define the value, the
+     * default from parameter is used.
+     * 
+     * @throws Exception 
+     */
+    public void testInheritedRead() throws Exception {
+        Preferences stored = new MapPreferences();
+        Preferences inherited = new MapPreferences();
+        
+        stored.put("key-1", "value-1");
+        stored.put("key-3", "override");
+        inherited.put("key-2", "value-2");
+        inherited.put("key-3", "base");
+
+        MemoryPreferences mem = MemoryPreferences.getWithInherited(this, 
inherited, stored);
+        Preferences test = mem.getPreferences();
+
+        assertEquals("Wrong value 1", "value-1", test.get("key-1", null));
+        assertEquals("Wrong value 2", "value-2", test.get("key-2", "a"));
+        assertEquals("Wrong value 3", "override", test.get("key-3", "a"));
+        assertEquals("Wrong value 4", "value-4", test.get("key-4", 
"value-4"));
+    }
+    
+    /**
+     * Asserts that if a value is remove()d during editing, the inherited 
value
+     * will be seen through. Also checks that the Preferences key is actually
+     * deleted on flush() and the inherited preferences is not altered.
+     */
+    public void testSeeInheritedThroughRemoves() throws Exception {
+        Preferences stored = new MapPreferences();
+        Preferences inherited = new MapPreferences();
+
+        stored.put("key", "value");
+        inherited.put("key", "parentValue");
+        
+        MemoryPreferences mem = MemoryPreferences.getWithInherited(this, 
inherited, stored);
+        Preferences test = mem.getPreferences();
+
+        assertEquals("Does not see local value", "value", test.get("key", 
null));
+        test.remove("key");
+        
+        assertEquals("Stored value changed prematurely", "value", 
stored.get("key", null));
+        assertEquals("Inherited not seen", "parentValue", test.get("key", 
null));
+        
+        test.flush();
+        assertNull("Stored value not erased", stored.get("key", null));
+        assertEquals("Inherited changed", "parentValue", test.get("key", 
null));
+    }
+
+    // 
-----------------------------------------------------------------------
+    // private implementation
+    // 
-----------------------------------------------------------------------
+    
+    private static class MapPreferences extends AbstractPreferences 
implements OverridePreferences {
+        
+        private Map<String,Object> map = new HashMap<String, Object>();
+
+        public MapPreferences() {
+            super(null, ""); // NOI18N
+        }
+
+        @Override
+        public boolean isOverriden(String key) {
+            return map.containsKey(key);
+        }
+        
+        protected void putSpi(String key, String value) {
+            map.put(key, value);            
+        }
+
+        protected String getSpi(String key) {
+            return (String)map.get(key);                    
+        }
+
+        protected void removeSpi(String key) {
+            map.remove(key);
+        }
+
+        protected void removeNodeSpi() throws BackingStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        protected String[] keysSpi() throws BackingStoreException {
+            String array[] = new String[map.keySet().size()];
+            return map.keySet().toArray( array );
+        }
+
+        protected String[] childrenNamesSpi() throws BackingStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        protected AbstractPreferences childSpi(String name) {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        protected void syncSpi() throws BackingStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+
+        protected void flushSpi() throws BackingStoreException {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+
+    private void write(Preferences prefs, String[] tree) {
+        for(String s : tree) {
+            int equalIdx = s.lastIndexOf('=');
+            assertTrue(equalIdx != -1);
+            String value = s.substring(equalIdx + 1);
+
+            String key;
+            String nodePath;
+            int slashIdx = s.lastIndexOf('/', equalIdx);
+            if (slashIdx != -1) {
+                key = s.substring(slashIdx + 1, equalIdx);
+                nodePath = s.substring(0, slashIdx);
+            } else {
+                key = s.substring(0, equalIdx);
+                nodePath = "";
+            }
+
+            Preferences node = prefs.node(nodePath);
+            node.put(key, value);
+        }
+    }
+
+    private void checkContains(Preferences prefs, String[] tree, String 
prefsId) throws BackingStoreException {
+        for(String s : tree) {
+            int equalIdx = s.lastIndexOf('=');
+            assertTrue(equalIdx != -1);
+            String value = s.substring(equalIdx + 1);
+
+            String key;
+            String nodePath;
+            int slashIdx = s.lastIndexOf('/', equalIdx);
+            if (slashIdx != -1) {
+                key = s.substring(slashIdx + 1, equalIdx);
+                nodePath = s.substring(0, slashIdx);
+            } else {
+                key = s.substring(0, equalIdx);
+                nodePath = "";
+            }
+
+            assertTrue(prefsId + " doesn't contain node '" + nodePath + "'", 
prefs.nodeExists(nodePath));
+            Preferences node = prefs.node(nodePath);
+
+            String realValue = node.get(key, null);
+            assertNotNull(prefsId + ", '" + nodePath + "' node doesn't 
contain key '" + key + "'", realValue);
+            assertEquals(prefsId + ", '" + nodePath + "' node, '" + key + "' 
contains wrong value", value, realValue);
+        }
+    }
+
+    private void checkNotContains(Preferences prefs, String[] tree, String 
prefsId) throws BackingStoreException {
+        for(String s : tree) {
+            int equalIdx = s.lastIndexOf('=');
+            assertTrue(equalIdx != -1);
+            String value = s.substring(equalIdx + 1);
+
+            String key;
+            String nodePath;
+            int slashIdx = s.lastIndexOf('/', equalIdx);
+            if (slashIdx != -1) {
+                key = s.substring(slashIdx + 1, equalIdx);
+                nodePath = s.substring(0, slashIdx);
+            } else {
+                key = s.substring(0, equalIdx);
+                nodePath = "";
+            }
+
+            if (prefs.nodeExists(nodePath)) {
+                Preferences node = prefs.node(nodePath);
+                String realValue = node.get(key, null);
+                if (realValue != null && realValue.equals(value)) {
+                    fail(prefsId + ", '" + nodePath + "' node contains key 
'" + key + "' = '" + realValue + "'");
+                }
+            }
+        }
+    }
+
+    private void dump(Preferences prefs, String prefsId) throws 
BackingStoreException {
+        for(String key : prefs.keys()) {
+            System.out.println(prefsId + ", " + prefs.absolutePath() + "/" + 
key + "=" + prefs.get(key, null));
+        }
+        for(String child : prefs.childrenNames()) {
+            dump(prefs.node(child), prefsId);
+        }
+    }
+
+    private void checkEquals(String msg, Preferences expected, Preferences 
test) throws BackingStoreException {
+        assertEquals("Won't compare two Preferences with different 
absolutePath", expected.absolutePath(), test.absolutePath());
+        
+        // check the keys and their values
+        for(String key : expected.keys()) {
+            String expectedValue = expected.get(key, null);
+            assertNotNull(msg + "; Expected:" + expected.absolutePath() + " 
has no '" + key + "'", expectedValue);
+            
+            String value = test.get(key, null);
+            assertNotNull(msg + "; Test:" + test.absolutePath() + " has no 
'" + key + "'", value);
+            assertEquals(msg + "; Test:" + test.absolutePath() + "/" + key + 
" has wrong value", expectedValue, value);
+        }
+
+        // check the children
+        for(String child : expected.childrenNames()) {
+            assertTrue(msg + "; Expected:" + expected.absolutePath() + " has 
no '" + child + "' subnode", expected.nodeExists(child));
+            Preferences expectedChild = expected.node(child);
+
+            assertTrue(msg + "; Test:" + test.absolutePath() + " has no '" + 
child + "' subnode", test.nodeExists(child));
+            Preferences testChild = test.node(child);
+
+            checkEquals(msg, expectedChild, testChild);
+        }
+    }
+
+    private void removeAllKidsAndKeys(Preferences prefs) throws 
BackingStoreException {
+        for(String kid : prefs.childrenNames()) {
+            prefs.node(kid).removeNode();
+        }
+        for(String key : prefs.keys()) {
+            prefs.remove(key);
+        }
+    }
+
+}

[hg] main-silver: Issue #226111 - Public support for Preferences...

Svata Dedic 03/03/2013

Project Features

About this Project

Editor was started in November 2009, is owned by Martin Ryzl, and has 147 members.
By use of this website, you agree to the NetBeans Policies and Terms of Use (revision 20140418.2d69abc). © 2013, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo
 
 
Close
loading
Please Confirm
Close