--- a/java.j2seproject/nbproject/project.xml +++ a/java.j2seproject/nbproject/project.xml @@ -171,7 +171,7 @@ 1 - 1.17 + 1.21 --- a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEProject.java +++ a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEProject.java @@ -260,6 +260,7 @@ new Info(), aux, helper.createCacheDirectoryProvider(), + helper.createAuxiliaryProperties(), spp, actionProvider, new J2SELogicalViewProvider(this, this.updateHelper, evaluator(), spp, refHelper), --- a/project.ant/apichanges.xml +++ a/project.ant/apichanges.xml @@ -105,6 +105,22 @@ + + + Added AntProjectHelper.createAuxiliaryProperties() + + + + + +

+ A new API method AntProjectHelper.createAuxiliaryProperties. +

+
+ + +
+ Support for project-specific libraries --- a/project.ant/manifest.mf +++ a/project.ant/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.project.ant/1 -OpenIDE-Module-Specification-Version: 1.20 +OpenIDE-Module-Specification-Version: 1.21 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/project/ant/Bundle.properties OpenIDE-Module-Install: org/netbeans/modules/project/ant/AntProjectModule.class --- a/project.ant/src/org/netbeans/spi/project/support/ant/AntProjectHelper.java +++ a/project.ant/src/org/netbeans/spi/project/support/ant/AntProjectHelper.java @@ -64,6 +64,7 @@ import org.netbeans.modules.project.ant.UserQuestionHandler; import org.netbeans.modules.project.ant.Util; import org.netbeans.spi.project.AuxiliaryConfiguration; +import org.netbeans.spi.project.AuxiliaryProperties; import org.netbeans.spi.project.CacheDirectoryProvider; import org.netbeans.spi.project.ProjectState; import org.netbeans.spi.queries.FileBuiltQueryImplementation; @@ -930,6 +931,21 @@ } /** + * Create an object permitting this project to expose {@link AuxiliaryProperties}. + * Would be placed into the project's lookup. + * + * This implementation places the properties into {@link #PROJECT_PROPERTIES_PATH} + * or {@link #PRIVATE_PROPERTIES_PATH} (depending on shared value). The properties are + * prefixed with "auxiliary.". + * + * @return an instance of {@link AuxiliaryProperties} suitable for the project lookup + * @since 1.21 + */ + public AuxiliaryProperties createAuxiliaryProperties() { + return new AuxiliaryPropertiesImpl(this); + } + + /** * Create an implementation of {@link org.netbeans.api.queries.FileBuiltQuery} that works with files * within the project based on simple glob pattern mappings. *

--- a/project.ant/src/org/netbeans/spi/project/support/ant/AuxiliaryPropertiesImpl.java +++ a/project.ant/src/org/netbeans/spi/project/support/ant/AuxiliaryPropertiesImpl.java @@ -0,0 +1,96 @@ +/* + * 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]" + * + * 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.spi.project.support.ant; +import java.util.LinkedList; +import java.util.List; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.spi.project.AuxiliaryProperties; +import org.openide.util.Mutex; + +/** + * + * @author Jan Lahoda + */ +class AuxiliaryPropertiesImpl implements AuxiliaryProperties { + + private final AntProjectHelper helper; + private final String propertyPrefix = "auxiliary."; + + public AuxiliaryPropertiesImpl(AntProjectHelper helper) { + this.helper = helper; + } + + public String get(String key, boolean shared) { + String location = shared ? AntProjectHelper.PROJECT_PROPERTIES_PATH : AntProjectHelper.PRIVATE_PROPERTIES_PATH; + EditableProperties props = helper.getProperties(location); + + return props.get(propertyPrefix + key); + } + + public void put(final String key, final String value, final boolean shared) { + ProjectManager.mutex().writeAccess(new Mutex.Action() { + public Void run() { + String location = shared ? AntProjectHelper.PROJECT_PROPERTIES_PATH : AntProjectHelper.PRIVATE_PROPERTIES_PATH; + EditableProperties props = helper.getProperties(location); + + props.put(propertyPrefix + key, value); + + helper.putProperties(location, props); + + return null; + } + }); + } + + public Iterable listKeys(boolean shared) { + List result = new LinkedList(); + String location = shared ? AntProjectHelper.PROJECT_PROPERTIES_PATH : AntProjectHelper.PRIVATE_PROPERTIES_PATH; + EditableProperties props = helper.getProperties(location); + + for (String k : props.keySet()) { + if (k.startsWith(propertyPrefix)) { + result.add(k.substring(propertyPrefix.length())); + } + } + + return result; + } + +} --- a/projectapi/apichanges.xml +++ a/projectapi/apichanges.xml @@ -104,6 +104,24 @@ + + +

Added ProjectUtils.getPreferences. + + + + + + +

+ Added new API method ProjectUtils.getPreferences and new SPI interface AuxiliaryProperties. +

+
+ + + +
+ Added support for composing project's lookup from multiple sources. --- a/projectapi/manifest.mf +++ a/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 --- a/projectapi/nbproject/project.xml +++ a/projectapi/nbproject/project.xml @@ -72,6 +72,14 @@
+ org.openide.modules + + + + 7.7 + + + org.openide.nodes @@ -92,23 +100,23 @@ unit + 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 --- a/projectapi/src/org/netbeans/api/project/ProjectUtils.java +++ a/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. + * + * @param project project for which preferences should be returned + * @param clazz 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 or null if the required + * {@link Preferences} are not supported for the given project. + * @since 1.16 + */ + public static Preferences getPreferences(Project project, Class clazz, boolean shared) { + Parameters.notNull("project", project); + Parameters.notNull("clazz", clazz); + + return AuxiliaryConfigBasedPreferencesProvider.getPreferences(project, clazz, 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 --- a/projectapi/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProvider.java +++ a/projectapi/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProvider.java @@ -0,0 +1,690 @@ +/* + * 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.Enumeration; +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.netbeans.spi.project.AuxiliaryProperties; +import org.openide.filesystems.FileObject; +import org.openide.modules.ModuleInfo; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +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> projects2SharedPrefs = new HashMap>(); + private static Map> projects2PrivatePrefs = new HashMap>(); + + static synchronized AuxiliaryConfigBasedPreferencesProvider findProvider(Project p, boolean shared) { + Map> target = shared ? projects2SharedPrefs : projects2PrivatePrefs; + Reference provRef = target.get(p); + AuxiliaryConfigBasedPreferencesProvider prov = provRef != null ? provRef.get() : null; + + if (prov != null) { + return prov; + } + + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryProperties ap = p.getLookup().lookup(AuxiliaryProperties.class); + + if (ac != null || ap != null) { + target.put(p, new CleaningWeakReference(prov = new AuxiliaryConfigBasedPreferencesProvider(p, ac, ap, shared), target, p)); + } else { + if (!shared) { + ap = new FallbackAuxiliaryPropertiesImpl(p.getProjectDirectory()); + target.put(p, new CleaningWeakReference(prov = new AuxiliaryConfigBasedPreferencesProvider(p, null, ap, shared), target, p)); + } + } + + return prov; + } + + public static Preferences getPreferences(Project project, Class clazz, boolean shared) { + AuxiliaryConfigBasedPreferencesProvider provider = findProvider(project, shared); + + if (provider == null) { + return null; + } + + return provider.findModule(AuxiliaryConfigBasedPreferencesProvider.findCNBForClass(clazz)); + } + + private static String encodeString(String s, Character additionalValid) { + StringBuilder result = new StringBuilder(); + + for (char c : s.toCharArray()) { + if (VALID_KEY_CHARACTERS.indexOf(c) != (-1) || (additionalValid != null && ((char) additionalValid) == c)) { + result.append(c); + } else { + result.append("_"); + result.append(Integer.toHexString((int) c)); + result.append("_"); + } + } + + return result.toString(); + } + + private static String decodeString(String s) { + StringBuilder result = new StringBuilder(); + String[] parts = s.split("_"); + + for (int cntr = 0; cntr < parts.length; cntr += 2) { + result.append(parts[cntr]); + + if (cntr + 1 < parts.length) { + result.append((char) Integer.parseInt(parts[cntr + 1], 16)); + } + } + + return result.toString(); + } + + static final String NAMESPACE = "http://www.netbeans.org/ns/auxiliary-configuration-preferences/1"; + + 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 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 AuxiliaryProperties ap; + private final boolean shared; + 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, AuxiliaryProperties ap, boolean shared) { + this.project = project; + this.ac = ac; + this.ap = ap; + this.shared = shared; + loadConfigRoot(); + } + + private void loadConfigRoot() { + if (ac == null) { + return ; + } + + Element configRootLoc = ac.getConfigurationFragment(EL_PREFERENCES, NAMESPACE, shared); + + if (configRootLoc == null) { + configRootLoc = XMLUtil.createDocument(EL_PREFERENCES, NAMESPACE, null, null).createElementNS(NAMESPACE, + EL_PREFERENCES); + } + + this.configRoot = configRootLoc; + } + + synchronized void flush() { + if (!modified) { + return ; + } + + boolean domModified = false; + + for (String removedNode : removedNodes) { + if (ac != null) { + Element el = findRelative(removedNode, false); + + if (el != null) { + el.getParentNode().removeChild(el); + } + + domModified = true; + } + + if (ap != null) { + String propName = toPropertyName(removedNode, ""); + + for (String key : ap.listKeys(shared)) { + if (key.startsWith(propName)) { + ap.put(key, null, shared); + } + } + } + } + + for (String createdNode : createdNodes) { + if (ap != null) { + String propName = toPropertyName(createdNode, ""); + + ap.put(propName, "", shared); + } else { + findRelative(createdNode, true); + + domModified = true; + } + } + + for (Entry> e : path2Data.entrySet()) { + if (ap != null) { + for (Entry value : e.getValue().entrySet()) { + ap.put(toPropertyName(e.getKey(), value.getKey()), value.getValue(), shared); + } + } else { + Element el = findRelative(e.getKey(), true); + + if (el != null) { + for (Entry value : e.getValue().entrySet()) { + Element p = find(el, value.getKey(), EL_PROPERTY, true); + + p.setAttribute(ATTR_VALUE, value.getValue()); + } + + domModified = true; + } + } + } + + for (Entry> e : path2Removed.entrySet()) { + if (ac != null) { + Element el = findRelative(e.getKey(), false); + + if (el != null) { + for (String removed : e.getValue()) { + Element p = find(el, removed, EL_PROPERTY, true); + + el.removeChild(p); + } + + domModified = true; + } + } + + if (ap != null) { + for (String removed : e.getValue()) { + ap.put(toPropertyName(e.getKey(), removed), "", shared); + } + } + } + + if (domModified) { + 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 cls) { + String absolutePath = null; + ClassLoader cl = cls.getClassLoader(); + for (ModuleInfo module : Lookup.getDefault().lookupAll(ModuleInfo.class)) { + if (module.isEnabled() && module.getClassLoader() == cl) { + absolutePath = module.getCodeNameBase(); + break; + } + } + if (absolutePath == null) { + absolutePath = cls.getName().replaceFirst("(^|\\.)[^.]+$", "");//NOI18N + } + assert absolutePath != null; + return absolutePath.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) { + if (ac == null) { + return null; + } + + 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 String toPropertyName(String path, String propertyName) { + return encodeString(path, '/').replace('/', '.') + '.' + encodeString(propertyName, null); + } + + 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; + } + + if (ap != null ) { + String keyProp = toPropertyName(path, key); + String res = AuxiliaryConfigBasedPreferencesProvider.this.ap.get(keyProp, shared); + + if (res != null) { + return res; + } + } + 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)); + } + + if (ap != null) { + String prefix = toPropertyName(path, ""); + + for (String key : ap.listKeys(shared)) { + if (key.startsWith(prefix)) { + String name = key.substring(prefix.length()); + + if (name.length() > 0 && name.indexOf('.') == (-1)) { + result.add(decodeString(name)); + } + } + } + } + + 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)); + } + } + + if (ap != null) { + String prefix = toPropertyName(path, ""); + + for (String key : ap.listKeys(shared)) { + if (key.startsWith(prefix)) { + String name = key.substring(prefix.length()); + + if (name.length() > 0 && name.indexOf('.') != (-1)) { + name = name.substring(0, name.indexOf('.')); + result.add(decodeString(name)); + } + } + } + } + + 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 FallbackAuxiliaryPropertiesImpl implements AuxiliaryProperties { + + private static final String PREFIX = "auxiliary."; + private FileObject projectDir; + + public FallbackAuxiliaryPropertiesImpl(FileObject projectDir) { + this.projectDir = projectDir; + } + + public String get(String key, boolean shared) { + assert !shared; + + Object v = projectDir.getAttribute(PREFIX + key); + + return v instanceof String ? (String) v : null; + } + + public void put(String key, String value, boolean shared) { + assert !shared; + + try { + projectDir.setAttribute(PREFIX + key, value); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + + public Iterable listKeys(boolean shared) { + assert !shared; + + List result = new LinkedList(); + + for (Enumeration en = projectDir.getAttributes(); en.hasMoreElements(); ) { + String key = en.nextElement(); + + if (key.startsWith(PREFIX)) { + key = key.substring(PREFIX.length()); + + if (get(key, shared) != null) { + result.add(key); + } + } + } + + return result; + } + + } + + 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); + } + } +} --- a/projectapi/src/org/netbeans/modules/projectapi/resources/auxiliary-configuration-preferences.xsd +++ a/projectapi/src/org/netbeans/modules/projectapi/resources/auxiliary-configuration-preferences.xsd @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --- a/projectapi/src/org/netbeans/spi/project/AuxiliaryProperties.java +++ a/projectapi/src/org/netbeans/spi/project/AuxiliaryProperties.java @@ -0,0 +1,84 @@ +/* + * 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]" + * + * 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.spi.project; + +/**

Allow to store arbitrary properties in the project, similarly as {@link AuxiliaryConfiguration}. + * Used as backing store for {@link org.netbeans.api.project.ProjectUtils#getPreferences(org.netbeans.api.project.Project, java.lang.Class, boolean)}. + *

+ * + *

Note to API clients: do not use this interface directly, use + * {@link org.netbeans.api.project.ProjectUtils#getPreferences(org.netbeans.api.project.Project, java.lang.Class, boolean)} instead. + *

+ * + * @author Jan Lahoda + * @since 1.16 + */ +public interface AuxiliaryProperties { + + /** + * Get a property value. + * + * @param key name of the property + * @param shared true to look in a sharable settings area, false to look in a private + * settings area + * @return value of the selected property, or null if not set. + */ + public String get(String key, boolean shared); + + /** + * Put a property value. + * + * @param key name of the property + * @param value value of the property. null will remove the property. + * @param shared true to look in a sharable settings area, false to look in a private + * settings area + */ + public void put(String key, String value, boolean shared); + + /** + * List all keys of all known properties. + * + * @param shared true to look in a sharable settings area, false to look in a private + * settings area + * @return known keys. + */ + public Iterable listKeys(boolean shared); + +} --- a/projectapi/test/unit/src/org/netbeans/api/project/TestUtil.java +++ a/projectapi/test/unit/src/org/netbeans/api/project/TestUtil.java @@ -52,6 +52,7 @@ import java.util.WeakHashMap; import junit.framework.Assert; import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.AuxiliaryConfiguration; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; import org.openide.filesystems.FileObject; @@ -60,7 +61,13 @@ import org.openide.filesystems.Repository; import org.openide.filesystems.URLMapper; import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; import org.openide.util.test.MockLookup; +import org.openide.xml.XMLUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; /** * Help set up org.netbeans.api.project.*Test. @@ -220,6 +227,11 @@ */ public static Object BROKEN_PROJECT_LOAD_LOCK = null; + /**If non-null, use the value as the Lookup for newly created projects. + * + */ + public static Lookup LOOKUP = null; + private static final class TestProjectFactory implements ProjectFactory { TestProjectFactory() {} @@ -246,7 +258,7 @@ } throw new IOException("Load failed of " + projectDirectory); } else { - return new TestProject(projectDirectory, state); + return new TestProject(projectDirectory, LOOKUP != null ? LOOKUP : Lookup.EMPTY, state); } } else { return null; @@ -279,17 +291,19 @@ private static final class TestProject implements Project { private final FileObject dir; + private final Lookup lookup; final ProjectState state; Throwable error; int saveCount = 0; - public TestProject(FileObject dir, ProjectState state) { + public TestProject(FileObject dir, Lookup lookup, ProjectState state) { this.dir = dir; + this.lookup = lookup; this.state = state; } public Lookup getLookup() { - return Lookup.EMPTY; + return lookup; } public FileObject getProjectDirectory() { --- a/projectapi/test/unit/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProviderTest.java +++ a/projectapi/test/unit/src/org/netbeans/modules/projectapi/AuxiliaryConfigBasedPreferencesProviderTest.java @@ -0,0 +1,405 @@ +/* + * 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.Collections; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +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.api.project.TestUtil; +import org.netbeans.junit.NbTestCase; +import org.netbeans.spi.project.AuxiliaryConfiguration; +import org.netbeans.spi.project.AuxiliaryProperties; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; +import org.openide.util.test.MockLookup; +import org.openide.xml.XMLUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * + * @author Jan Lahoda + */ +public class AuxiliaryConfigBasedPreferencesProviderTest extends NbTestCase { + + public AuxiliaryConfigBasedPreferencesProviderTest(String testName) { + super(testName); + } + + private FileObject fo; + private Project p; + private TestLookup lookup; + + @Override + protected void setUp() throws Exception { + MockLookup.setInstances(TestUtil.testProjectFactory()); + clearWorkDir(); + File wd = getWorkDir(); + FileUtil.refreshAll(); + File f = new File(new File(wd, "test"), "testproject"); + FileObject testprojectFO = FileUtil.createFolder(f); + assertNotNull(testprojectFO); + fo = testprojectFO.getParent(); + TestUtil.LOOKUP = lookup = new TestLookup(); + p = ProjectManager.getDefault().findProject(fo); + assertNotNull(p); + } + + public void testStorage() throws IOException, BackingStoreException { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl())); + doTestStorage(); + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryProperties())); + doTestStorage(); + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl(), new TestAuxiliaryProperties())); + doTestStorage(); + } + + private void doTestStorage() throws IOException, BackingStoreException { + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryProperties ap = p.getLookup().lookup(AuxiliaryProperties.class); + + assertTrue(ac != null || ap != null); + + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac, ap, true); + 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, ap, true); + 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 { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl())); + + final AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + + assertNotNull(ac); + + 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, null, true); + 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 { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl())); + doTestSubnodes(); + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryProperties())); + doTestSubnodes(); + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl(), new TestAuxiliaryProperties())); + doTestSubnodes(); + } + + private void doTestSubnodes() throws IOException, BackingStoreException { + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + AuxiliaryProperties ap = p.getLookup().lookup(AuxiliaryProperties.class); + + assertTrue(ac != null || ap != null); + + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, ac, ap, true); + 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, ap, true); + pref = provider.findModule("test"); + + assertTrue(pref.node("subnode1").nodeExists("subnode2")); + 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, ap, true); + 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 { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl())); + + AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); + + assertNotNull(ac); + + AuxiliaryConfigBasedPreferencesProvider toSync = new AuxiliaryConfigBasedPreferencesProvider(p, ac, null, true); + Preferences pref = toSync.findModule("test"); + + pref.put("test", "test"); + + pref.node("subnode1/subnode2").put("somekey", "somevalue"); + pref.flush(); + + AuxiliaryConfigBasedPreferencesProvider orig = new AuxiliaryConfigBasedPreferencesProvider(p, ac, null, true); + + 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, InterruptedException { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryConfigurationImpl())); + + 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); + + TestUtil.notifyDeleted(p); + + Thread.sleep(5000); + + p = null; + pref = null; + + assertGC("", rPref); + assertGC("", rProject); + } + + public void testComplexNames() throws IOException, BackingStoreException, InterruptedException { + lookup.setDelegates(Lookups.fixed(new TestAuxiliaryProperties())); + + AuxiliaryProperties ap = p.getLookup().lookup(AuxiliaryProperties.class); + + assertNotNull(ap != null); + + AuxiliaryConfigBasedPreferencesProvider provider = new AuxiliaryConfigBasedPreferencesProvider(p, null, ap, true); + Preferences pref = provider.findModule("test"); + + pref.node(".:./.:.").put(".:.", "correct"); + + pref.flush(); + + provider = new AuxiliaryConfigBasedPreferencesProvider(p, null, ap, true); + pref = provider.findModule("test"); + + assertTrue(pref.nodeExists(".:./.:.")); + assertEquals(Arrays.asList(".:."), Arrays.asList(pref.node(".:./.:.").keys())); + } + + public void testNoAuxiliaryImplInLookup() { + Preferences pref = AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, Object.class, true); + + assertNull(pref); + + pref = AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, Object.class, false); + + assertNotNull(pref); + + pref = AuxiliaryConfigBasedPreferencesProvider.getPreferences(p, Object.class, true); + + assertNull(pref); + } + + private static final class TestAuxiliaryConfigurationImpl implements AuxiliaryConfiguration { + + private final Document sharedDOM; + private final Document privDOM; + + public TestAuxiliaryConfigurationImpl() { + sharedDOM = XMLUtil.createDocument("test", null, null, null); + privDOM = XMLUtil.createDocument("test", null, null, null); + } + + public Element getConfigurationFragment(String elementName, String namespace, boolean shared) { + Element el = find(shared, namespace, elementName); + + if (el != null) { + Document dummy = XMLUtil.createDocument("test", null, null, null); + return (Element) dummy.importNode(el, true); + } + + return null; + } + + public void putConfigurationFragment(Element fragment, boolean shared) throws IllegalArgumentException { + removeConfigurationFragment(fragment.getLocalName(), fragment.getNamespaceURI(), shared); + + Document dom = shared ? sharedDOM : privDOM; + + dom.getDocumentElement().appendChild(dom.importNode(fragment, true)); + } + + public boolean removeConfigurationFragment(String elementName, String namespace, boolean shared) throws IllegalArgumentException { + Element el = find(shared, namespace, elementName); + + if (el != null) { + el.getParentNode().removeChild(el); + return true; + } + + return false; + } + + private Element find(boolean shared, String namespace, String elementName) { + Document dom = shared ? sharedDOM : privDOM; + NodeList nl = dom.getDocumentElement().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())) { + return (Element) n; + } + } + return null; + } + + } + + private static final class TestAuxiliaryProperties implements AuxiliaryProperties { + + private Properties pub; + private Properties priv; + + public TestAuxiliaryProperties() { + this.pub = new Properties(); + this.priv = new Properties(); + } + + public String get(String key, boolean shared) { + return (shared ? pub : priv).getProperty(key); + } + + public void put(String key, String value, boolean shared) { + if (value != null) { + (shared ? pub : priv).setProperty(key, value); + } else { + (shared ? pub : priv).remove(key); + } + } + + public Iterable listKeys(boolean shared) { + Enumeration en = (shared ? pub : priv).propertyNames(); + List result = new LinkedList(); + + while (en.hasMoreElements()) { + Object el = en.nextElement(); + + if (el instanceof String) { + result.add((String) el); + } + } + + return Collections.unmodifiableList(result); + } + + } + + private static final class TestLookup extends ProxyLookup { + public void setDelegates(Lookup... l) { + setLookups(l); + } + } +}