diff --git a/projectapi/apichanges.xml b/projectapi/apichanges.xml --- a/projectapi/apichanges.xml +++ b/projectapi/apichanges.xml @@ -104,6 +104,23 @@ + + + Added ProjectUtils.getPreferences. + + + + + + +

+ Added ProjectUtils.getPreferences. +

+
+ + +
+ Added support for composing project's lookup from multiple sources. diff --git a/projectapi/manifest.mf b/projectapi/manifest.mf --- a/projectapi/manifest.mf +++ b/projectapi/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.projectapi/1 -OpenIDE-Module-Specification-Version: 1.15 +OpenIDE-Module-Specification-Version: 1.16 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/projectapi/Bundle.properties diff --git a/projectapi/nbproject/project.xml b/projectapi/nbproject/project.xml --- a/projectapi/nbproject/project.xml +++ b/projectapi/nbproject/project.xml @@ -92,23 +92,28 @@ unit + org.netbeans.modules.java.j2seproject + + + + + org.netbeans.modules.masterfs + + org.netbeans.modules.projectapi - org.openide.modules - - - org.netbeans.modules.masterfs - - - org.openide.util + org.openide.filesystems - org.openide.filesystems + org.openide.modules + + + org.openide.util diff --git a/projectapi/src/org/netbeans/api/project/ProjectUtils.java b/projectapi/src/org/netbeans/api/project/ProjectUtils.java --- a/projectapi/src/org/netbeans/api/project/ProjectUtils.java +++ b/projectapi/src/org/netbeans/api/project/ProjectUtils.java @@ -47,12 +47,15 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.prefs.Preferences; import javax.swing.Icon; import javax.swing.ImageIcon; +import org.netbeans.modules.projectapi.AuxiliaryConfigBasedPreferencesProvider; import org.netbeans.spi.project.SubprojectProvider; import org.netbeans.spi.project.support.GenericSources; import org.openide.filesystems.FileStateInvalidException; import org.openide.util.Mutex; +import org.openide.util.Parameters; import org.openide.util.Utilities; /** @@ -143,6 +146,23 @@ } /** + * Return {@link Preferences} for the given project and given module (specified + * as using {@link Class} as in {@link org.openide.util.NbPreferences#forModule(java.lang.Class)}). + * + * @param p for which project should be the preferences returned + * @param c module specification as in {@link org.openide.util.NbPreferences#forModule(java.lang.Class)} + * @param shared whether the returned settings should be shared + * @return {@link Preferences} for the given project + * @since 1.16 + */ + public static Preferences getPreferences(Project p, Class c, boolean shared) { + Parameters.notNull("p", p); + Parameters.notNull("c", c); + + return AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, c, shared); + } + + /** * Do a DFS traversal checking for cycles. * @param encountered projects already encountered in the DFS (added and removed as you go) * @param curr current node to visit diff --git a/projectapi/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProvider.java b/projectapi/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProvider.java new file mode 100644 --- /dev/null +++ b/projectapi/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProvider.java @@ -0,0 +1,543 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 2008 Sun + * Microsystems, Inc. All Rights Reserved. + * + * 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. + */ + +package org.netbeans.modules.projectapi; + +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.prefs.AbstractPreferences; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.spi.project.AuxiliaryConfiguration; +import org.openide.filesystems.FileStateInvalidException; +import org.openide.util.Exceptions; +import org.openide.util.NbPreferences; +import org.openide.util.Parameters; +import org.openide.util.RequestProcessor; +import org.openide.util.RequestProcessor.Task; +import org.openide.util.Utilities; +import org.openide.xml.XMLUtil; +import org.w3c.dom.DOMException; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * @author Jan Lahoda + */ +public class AuxiliaryConfigBasedPreferencesProvider { + + private static Map> projects = new HashMap>(); + + static synchronized AuxiliaryConfigBasedPreferencesProvider findProvider(Project p) { + Reference provRef = projects.get(p); + AuxiliaryConfigBasedPreferencesProvider prov = provRef != null ? provRef.get() : null; + + if (prov != null) { + return prov; + } + + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + + if (ac != null) { + projects.put(p, new CleaningWeakReference(prov = new AuxiliaryConfigBasedPreferencesProvider(p, ac), projects, p)); + } + + return prov; + } + + public static Preferences getPreferences(Project p, Class c, boolean shared) { + Parameters.notNull("p", p); + Parameters.notNull("c", c); + + return shared ? getSharedSettings(p, c) : getPrivateSettings(p, c); + } + + private static Preferences getSharedSettings(Project p, Class c) { + AuxiliaryConfigBasedPreferencesProvider provider = findProvider(p); + + if (provider == null) { + return null; + } + + return provider.findModule(AuxiliaryConfigBasedPreferencesProvider.findCNBForClass(c)); + } + + private static synchronized Preferences getPrivateSettings(Project p, Class c) { + Preferences prefs = NbPreferences.forModule(AuxiliaryConfigBasedPreferencesProvider.class); + + prefs = prefs.node(PREF_NODE_AUXILIARY_CONFIG_BASE_PREFERENCES); + + try { + String projectDir = encodeString(p.getProjectDirectory().getURL().toExternalForm()); + String projectPrivateKey = prefs.get(projectDir, null); + + if (projectPrivateKey == null) { + //find one: + int key = 0; + + while (prefs.getBoolean(projectPrivateKey = "__id" + Integer.toHexString(key), false)) + key++; + + prefs.put(projectDir, projectPrivateKey); + prefs.putBoolean(projectPrivateKey, true); + } + + return prefs.node(projectPrivateKey); + } catch (FileStateInvalidException ex) { + Exceptions.printStackTrace(ex); + return null; + } + } + + private static String encodeString(String s) { + StringBuilder result = new StringBuilder(); + + for (char c : s.toCharArray()) { + if (VALID_KEY_CHARACTERS.indexOf(c) != (-1)) { + result.append(c); + } else { + result.append("_"); + result.append(Integer.toHexString((int) c)); + result.append("_"); + } + } + + return result.toString(); + } + + private static final String NAMESPACE = "http://www.netbeans.org/ns/auxiliary-configuration-preferences/1"; + + private static final String EL_PREFERENCES = "preferences"; + private static final String EL_MODULE = "module"; + private static final String EL_PROPERTY = "property"; + private static final String EL_NODE = "node"; + + private static final String ATTR_NAME = "name"; + private static final String ATTR_VALUE = "value"; + + private static final String PREF_NODE_AUXILIARY_CONFIG_BASE_PREFERENCES = "auxiliaryConfigBasedPreferences"; + private static final String VALID_KEY_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTVUWXYZabcdefghijklmnopqrstvuwxyz0123456789"; + + private static final RequestProcessor WORKER = new RequestProcessor("AuxiliaryConfigBasedPreferencesProvider worker", 1); + private static final int AUTOFLUSH_TIMEOUT = 5000; + + private final Project project; + private final AuxiliaryConfiguration ac; + private final Map> module2Preferences = new HashMap>(); + private Element configRoot; + private boolean modified; + private final Task autoFlushTask = WORKER.create(new Runnable() { + public void run() { + flush(); + } + }); + + private final Map> path2Data = new HashMap>(); + private final Map> path2Removed = new HashMap>(); + private final Set removedNodes = new HashSet(); + private final Set createdNodes = new HashSet(); + + AuxiliaryConfigBasedPreferencesProvider(Project project, AuxiliaryConfiguration ac) { + this.project = project; + this.ac = ac; + loadConfigRoot(); + } + + private void loadConfigRoot() { + Element configRootLoc = ac.getConfigurationFragment(EL_PREFERENCES, NAMESPACE, true); + + if (configRootLoc == null) { + configRootLoc = XMLUtil.createDocument(EL_PREFERENCES, NAMESPACE, null, null).createElementNS(NAMESPACE, + EL_PREFERENCES); + } + + this.configRoot = configRootLoc; + } + + synchronized void flush() { + if (!modified) { + return ; + } + + for (String removedNode : removedNodes) { + Element el = findRelative(removedNode, false); + + if (el != null) { + el.getParentNode().removeChild(el); + } + } + + for (String createdNode : createdNodes) { + findRelative(createdNode, true); + } + + for (Entry> e : path2Data.entrySet()) { + Element el = findRelative(e.getKey(), false); + + assert el != null; + + for (Entry value : e.getValue().entrySet()) { + Element p = find(el, value.getKey(), EL_PROPERTY, true); + + p.setAttribute(ATTR_VALUE, value.getValue()); + } + } + + for (Entry> e : path2Removed.entrySet()) { + Element el = findRelative(e.getKey(), false); + + assert el != null; + + for (String removed : e.getValue()) { + Element p = find(el, removed, EL_PROPERTY, true); + + el.removeChild(p); + } + } + + ac.putConfigurationFragment(configRoot, true); + + try { + ProjectManager.getDefault().saveProject(project); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + + path2Data.clear(); + path2Removed.clear(); + removedNodes.clear(); + createdNodes.clear(); + modified = false; + } + + synchronized void sync() { + loadConfigRoot(); + flush(); + } + + private void markModified() { + autoFlushTask.cancel(); + autoFlushTask.schedule(AUTOFLUSH_TIMEOUT); + modified = true; + } + + private static String findCNBForClass(Class c) { + Preferences p = NbPreferences.forModule(c); + + return p.absolutePath().replaceFirst("/", "").replace('/', '.'); + } + + public synchronized Preferences findModule(String moduleName) { + Reference prefRef = module2Preferences.get(moduleName); + AuxiliaryConfigBasedPreferences pref = prefRef != null ? prefRef.get() : null; + + if (pref == null) { + module2Preferences.put(moduleName, new CleaningWeakReference(pref = new AuxiliaryConfigBasedPreferences(null, "", moduleName), module2Preferences, moduleName)); + } + + return pref; + } + + private Element findRelative(String path, boolean createIfMissing) { + String[] sep = path.split("/"); + + assert sep.length > 0; + + Element e = find(configRoot, sep[0], EL_MODULE, createIfMissing); + + for (int cntr = 1; cntr < sep.length && e != null; cntr++) { + e = find(e, sep[cntr], EL_NODE, createIfMissing); + } + + return e; + } + + private Map getData(String path) { + Map data = path2Data.get(path); + + if (data == null) { + path2Data.put(path, data = new HashMap()); + } + + return data; + } + + private Set getRemoved(String path) { + Set removed = path2Removed.get(path); + + if (removed == null) { + path2Removed.put(path, removed = new HashSet()); + } + + return removed; + } + + private void removeNode(String path) { + path2Data.remove(path); + path2Removed.remove(path); + createdNodes.remove(path); + removedNodes.add(path); + } + + private boolean isRemovedNode(String path) { + return removedNodes.contains(path); + } + + private static Element find(Element dom, String key, String elementName, boolean createIfMissing) { + NodeList nl = dom.getChildNodes(); + + for (int cntr = 0; cntr < nl.getLength(); cntr++) { + Node n = nl.item(cntr); + + if (n.getNodeType() == Node.ELEMENT_NODE && NAMESPACE.equals(n.getNamespaceURI()) && elementName.equals(n.getLocalName())) { + if (key.equals(((Element) n).getAttribute(ATTR_NAME))) { + return (Element) n; + } + } + } + + if (!createIfMissing) { + return null; + } + + Element el = dom.getOwnerDocument().createElementNS(NAMESPACE, elementName); + + el.setAttribute(ATTR_NAME, key); + + dom.appendChild(el); + + return el; + } + + private class AuxiliaryConfigBasedPreferences extends AbstractPreferences { + + private final String path; + + public AuxiliaryConfigBasedPreferences(AbstractPreferences parent, String name, String path) { + super(parent, name); + this.path = path; + } + + @Override + protected void putSpi(String key, String value) { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + getData(path).put(key, value); + getRemoved(path).remove(key); + + markModified(); + } + } + + @Override + protected String getSpi(String key) { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + if (getRemoved(path).contains(key)) { + return null; + } + + if (getData(path).containsKey(key)) { + return getData(path).get(key); + } + + if (isRemovedNode(path)) { + return null; + } + + Element p = findRelative(path, false); + + p = p != null ? AuxiliaryConfigBasedPreferencesProvider.find(p, key, EL_PROPERTY, false) : null; + + if (p == null) { + return null; + } + + return p.getAttribute(ATTR_VALUE); + } + } + + @Override + protected void removeSpi(String key) { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + getData(path).remove(key); + getRemoved(path).add(key); + + markModified(); + } + } + + @Override + protected void removeNodeSpi() throws BackingStoreException { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + AuxiliaryConfigBasedPreferencesProvider.this.removeNode(path); + markModified(); + } + } + + @Override + protected String[] keysSpi() throws BackingStoreException { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + Collection result = new LinkedHashSet(); + + if (!isRemovedNode(path)) { + result.addAll(list(EL_PROPERTY)); + } + + result.addAll(getData(path).keySet()); + result.removeAll(getRemoved(path)); + + return result.toArray(new String[0]); + } + } + + @Override + protected String[] childrenNamesSpi() throws BackingStoreException { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + return getChildrenNames().toArray(new String[0]); + } + } + + @Override + protected AbstractPreferences childSpi(String name) { + synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { + String nuePath = path + "/" + name; + if (!getChildrenNames().contains("name")) { + AuxiliaryConfigBasedPreferencesProvider.this.createdNodes.add(nuePath); + } + + return new AuxiliaryConfigBasedPreferences(this, name, nuePath); + } + } + + @Override + public void sync() throws BackingStoreException { + AuxiliaryConfigBasedPreferencesProvider.this.sync(); + } + + @Override + protected void syncSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Should never be called."); + } + + @Override + public void flush() throws BackingStoreException { + AuxiliaryConfigBasedPreferencesProvider.this.flush(); + } + + @Override + protected void flushSpi() throws BackingStoreException { + throw new UnsupportedOperationException("Should never be called."); + } + + private Collection getChildrenNames() { + Collection result = new LinkedHashSet(); + + if (!isRemovedNode(path)) { + result.addAll(list(EL_NODE)); + } + + for (String removed : removedNodes) { + int slash = removed.lastIndexOf('/'); + + if (path.equals(removed.substring(slash))) { + result.remove(removed.substring(slash + 1)); + } + } + for (String created : createdNodes) { + int slash = created.lastIndexOf('/'); + + if (path.equals(created.substring(slash))) { + result.add(created.substring(slash + 1)); + } + } + + return result; + } + + private Collection list(String elementName) throws DOMException { + Element dom = findRelative(path, false); + + if (dom == null) { + return Collections.emptyList(); + } + + List names = new LinkedList(); + NodeList nl = dom.getElementsByTagNameNS(NAMESPACE, elementName); + + for (int cntr = 0; cntr < nl.getLength(); cntr++) { + Node n = nl.item(cntr); + + names.add(((Element) n).getAttribute(ATTR_NAME)); + } + + return names; + } + + } + + private static final class CleaningWeakReference extends WeakReference implements Runnable { + private final Map map; + private final Object key; + + public CleaningWeakReference(T data, Map map, Object key) { + super(data, Utilities.activeReferenceQueue()); + this.map = map; + this.key = key; + } + + public void run() { + map.remove(key); + } + } +} diff --git a/projectapi/src/org/netbeans/modules/projectapi/resources/auxiliary-configuration-preferences.xsd b/projectapi/src/org/netbeans/modules/projectapi/resources/auxiliary-configuration-preferences.xsd new file mode 100644 --- /dev/null +++ b/projectapi/src/org/netbeans/modules/projectapi/resources/auxiliary-configuration-preferences.xsd @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projectapi/test/unit/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProviderTest.java b/projectapi/test/unit/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProviderTest.java new file mode 100644 --- /dev/null +++ b/projectapi/test/unit/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProviderTest.java @@ -0,0 +1,210 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 2008 Sun + * Microsystems, Inc. All Rights Reserved. + * + * 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. + */ + +package org.netbeans.modules.projectapi; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.j2seproject.J2SEProjectGenerator; +import org.netbeans.spi.project.AuxiliaryConfiguration; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.w3c.dom.Element; + +/** + * + * @author Jan Lahoda + */ +public class AuxiliaryConfigBasedPreferencesProviderTest extends NbTestCase { + + public AuxiliaryConfigBasedPreferencesProviderTest(String testName) { + super(testName); + } + + private FileObject fo; + private Project p; + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + File wd = getWorkDir(); + FileUtil.refreshAll(); + File f = new File(wd, "test"); + J2SEProjectGenerator.createProject(f, "test", null, null, null); + fo = FileUtil.toFileObject(f); + assertNotNull(fo); + p = ProjectManager.getDefault().findProject(fo); + assertNotNull(p); + } + + public void testStorage() throws IOException, BackingStoreException { + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + Preferences pref = provider.findModule("test"); + + pref.put("test", "test"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue"); + + assertEquals(Arrays.asList("somekey"), Arrays.asList(pref.node("subnode1/subnode2").keys())); + + pref.flush(); + + provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + pref = provider.findModule("test"); + + assertEquals("test", pref.get("test", null)); + assertEquals("somevalue", pref.node("subnode1/subnode2").get("somekey", null)); + assertEquals(Arrays.asList("somekey"), Arrays.asList(pref.node("subnode1/subnode2").keys())); + pref.node("subnode1/subnode2").remove("somekey"); + assertEquals(Arrays.asList(), Arrays.asList(pref.node("subnode1/subnode2").keys())); + } + + public void testNoSaveWhenNotModified() throws IOException, BackingStoreException { + final AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + final AtomicInteger putCount = new AtomicInteger(); + + AuxiliaryConfiguration newAC = new AuxiliaryConfiguration() { + public Element getConfigurationFragment(String elementName, String namespace, boolean shared) { + return ac.getConfigurationFragment(elementName, namespace, shared); + } + public void putConfigurationFragment(Element fragment, boolean shared) throws IllegalArgumentException { + putCount.incrementAndGet(); + ac.putConfigurationFragment(fragment, shared); + } + public boolean removeConfigurationFragment(String elementName, String namespace, boolean shared) throws IllegalArgumentException { + return ac.removeConfigurationFragment(elementName, namespace, shared); + } + }; + + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, newAC); + Preferences pref = provider.findModule("test"); + + pref.put("test", "test"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue"); + + assertEquals(0, putCount.get()); + pref.flush(); + assertEquals(1, putCount.get()); + pref.flush(); + assertEquals(1, putCount.get()); + } + + public void testSubnodes() throws IOException, BackingStoreException { + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + Preferences pref = provider.findModule("test"); + + pref.put("test", "test"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue1"); + pref.node("subnode1").put("somekey", "somevalue2"); + + pref.flush(); + + provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + pref = provider.findModule("test"); + + assertEquals("somevalue1", pref.node("subnode1/subnode2").get("somekey", null)); + assertEquals("somevalue2", pref.node("subnode1").get("somekey", null)); + pref.node("subnode1").removeNode(); + assertEquals(null, pref.node("subnode1/subnode2").get("somekey", null)); + assertEquals(null, pref.node("subnode1").get("somekey", null)); + + pref.flush(); + + provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + pref = provider.findModule("test"); + + assertEquals(null, pref.node("subnode1/subnode2").get("somekey", null)); + assertEquals(null, pref.node("subnode1").get("somekey", null)); + } + + public void testSync() throws IOException, BackingStoreException { + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryConfigBasedPreferencesProvider toSync = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + Preferences pref = toSync.findModule("test"); + + pref.put("test", "test"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue"); + pref.flush(); + + AuxiliaryConfigBasedPreferencesProvider orig = new AuxiliaryConfigBasedPreferencesProvider(p, ac); + + Preferences origNode = orig.findModule("test").node("subnode1/subnode2"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue2"); + pref.flush(); + + origNode.sync(); + + assertEquals("somevalue2", origNode.get("somekey", null)); + } + + public void testReclaimable() throws IOException, BackingStoreException { + Preferences pref = AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, Object.class, true); + + //the same preferences instance is returned as long as the previous one exists: + assertTrue(pref == AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, Object.class, true)); + + //but the preferences can be reclaimed, as well as the project if noone holds them: + Reference rPref = new WeakReference(pref); + Reference rProject = new WeakReference(p); + + p = null; + pref = null; + + assertGC("", rPref); + assertGC("", rProject); + } + +}