# HG changeset patch # Parent ab27872cd0c2318a578d72e76e21929171ee1807 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 @@ + Detect preferences override and support inheritance + + + + + +

+ OverridePreferences can be used to detect whether the setting is defined for + the specific MIME type, or inherited from default ('all languages') settings. +

+

+ During options editing, MemoryPreferences can be used to create Preferences + object, that propagates changes to its (persistent) delegate upon flush(). +

+
+ + + +
+ + EditorSettings.PROP_MIME_TYPES 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. + *

+ * 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}. + *

+ * The returned Preferences object implements {@link LocalPreferences} extension + * interface. + *

+ * This implementation does not support 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. + *

+ * 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. + *

+ * This interface is implemented on Editor settings Preferences objects + * stored in MimeLookup (can be obtained by MimeLookup.getLookup(mime).lookup(Preferences.class)). + * + * @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. + *

+ * Let us assume the following assignment: + *

    + *
  • TC (this current) = currently added/changed/removed values + *
  • TP (this persistent) = persistent values, the getLocal() part of the Mime PreferencesImpl object + *
  • PC (parent current) = currently added/changed/removed values of the parent + *
  • PP (parent persistent) = persistent values, the getLocal() part of the parent MimePreferences + *
+ * 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, ...) + *

+ * 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 ignorePut = new ThreadLocal(); + + @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 names = new HashSet(); + 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 @@ -154,6 +155,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) { 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 bag = null; + + synchronized (tree.treeLock()) { + checkNotNull(key, "key"); //NOI18N + checkRemoved(); + + if (removedKeys.add(key)) { + data.remove(key); + bag = new EventBag(); + 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 bag = new EventBag(); + + synchronized (tree.treeLock()) { + checkRemoved(); + + // Determine modified or added keys + Set keys = new HashSet(); + 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 keys = new HashSet(); + 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 names = new HashSet(); + 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> events = new LinkedList>(); + + 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> prefEvents = new ArrayList>(); + ArrayList> nodeEvents = new ArrayList>(); + + 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 data = new HashMap(); + private final Set removedKeys = new HashSet(); + private final Map children = new HashMap(); + private final Set removedChildren = new HashSet(); + + private boolean noEvents = false; + private PreferenceChangeListener weakPrefListener; + private final Set prefListeners = new HashSet(); + private NodeChangeListener weakNodeListener; + private final Set nodeListeners = new HashSet(); + + 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 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(); + 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> 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 bag = new EventBag(); + 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> prefEvents, + List> nodeEvents + ) { + // synchronize all children firts + for(ProxyPreferencesImpl pp : children.values()) { + pp._sync(prefEvents, nodeEvents); + } + + // report all new children as removed + EventBag nodeBag = new EventBag(); + 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 prefBag = new EventBag(); + 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> events) { + if (noEvents) { + return; + } + + for(EventBag 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> events) { + if (noEvents) { + return; + } + + for(EventBag 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 forest = trees.get(token); + if (forest == null) { + forest = new HashMap(); + 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> trees = new WeakHashMap>(); + + private final Preferences root; + private final Reference tokenRef; + private final Map nodes = new HashMap(); + + private Tree(Object token, Preferences root) { + this.root = root; + this.tokenRef = new WeakReference(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 { + private final Set listeners = new HashSet(); + private final Set events = new HashSet(); + + public EventBag() { + } + + public Set getListeners() { + return listeners; + } + + public Set getEvents() { + return events; + } + + public void addListeners(Collection 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 treeTokenRef = new WeakReference(treeToken); + Reference testRef = new WeakReference(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 map = new HashMap(); + + 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); + } + } + +}