/******************************************************************************* * Copyright (c) 2004, 2010 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Markus Schorn (Wind River) - [108066] Project prefs marked dirty on read *******************************************************************************/ package org.eclipse.core.internal.resources; import java.io.*; import java.util.*; import org.eclipse.core.internal.preferences.EclipsePreferences; import org.eclipse.core.internal.preferences.ExportedPreferences; import org.eclipse.core.internal.utils.*; import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.MultiRule; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IExportedPreferences; import org.eclipse.osgi.util.NLS; import org.osgi.service.prefs.BackingStoreException; import org.osgi.service.prefs.Preferences; /** * Represents a node in the Eclipse preference hierarchy which stores preference values for * projects. * * @since 3.0 */ public class ProjectPreferences extends EclipsePreferences { class SortedProperties extends Properties { class IteratorWrapper implements Enumeration { Iterator iterator; public IteratorWrapper(Iterator iterator) { this.iterator= iterator; } public boolean hasMoreElements() { return iterator.hasNext(); } public Object nextElement() { return iterator.next(); } } private static final long serialVersionUID= 1L; /* (non-Javadoc) * @see java.util.Hashtable#keys() */ public synchronized Enumeration keys() { TreeSet set= new TreeSet(); for (Enumeration e= super.keys(); e.hasMoreElements();) set.add(e.nextElement()); return new IteratorWrapper(set.iterator()); } } /** * Cache which nodes have been loaded from disk */ protected static Set loadedNodes= Collections.synchronizedSet(new HashSet()); private IFile file; private boolean initialized= false; /** * Flag indicating that this node is currently reading values from disk, to avoid flushing * during a read. */ private boolean isReading; /** * Flag indicating that this node is currently writing values to disk, to avoid re-reading after * the write completes. */ private boolean isWriting; private IEclipsePreferences loadLevel; private IProject project; private String qualifier; // cache private int segmentCount; static void deleted(IFile file) throws CoreException { IPath path= file.getFullPath(); int count= path.segmentCount(); if (count != 3) return; // check if we are in the .settings directory if (!EclipsePreferences.DEFAULT_PREFERENCES_DIRNAME.equals(path.segment(1))) return; Preferences root= Platform.getPreferencesService().getRootNode(); String project= path.segment(0); String qualifier= path.removeFileExtension().lastSegment(); ProjectPreferences projectNode= (ProjectPreferences)root.node(ProjectScope.SCOPE).node(project); // if the node isn't known then just return try { if (!projectNode.nodeExists(qualifier)) return; } catch (BackingStoreException e) { // ignore } // clear the preferences clearNode(projectNode.node(qualifier)); // notifies the CharsetManager if needed if (qualifier.equals(ResourcesPlugin.PI_RESOURCES)) preferencesChanged(file.getProject()); } static void deleted(IFolder folder) throws CoreException { IPath path= folder.getFullPath(); int count= path.segmentCount(); if (count != 2) return; // check if we are the .settings directory if (!EclipsePreferences.DEFAULT_PREFERENCES_DIRNAME.equals(path.segment(1))) return; Preferences root= Platform.getPreferencesService().getRootNode(); // The settings dir has been removed/moved so remove all project prefs // for the resource. String project= path.segment(0); Preferences projectNode= root.node(ProjectScope.SCOPE).node(project); // check if we need to notify the charset manager boolean hasResourcesSettings= getFile(folder, ResourcesPlugin.PI_RESOURCES).exists(); // remove the preferences removeNode(projectNode); // notifies the CharsetManager if (hasResourcesSettings) preferencesChanged(folder.getProject()); } /* * The whole project has been removed so delete all of the project settings */ static void deleted(IProject project) throws CoreException { // The settings dir has been removed/moved so remove all project prefs // for the resource. We have to do this now because (since we aren't // synchronizing) there is short-circuit code that doesn't visit the // children. Preferences root= Platform.getPreferencesService().getRootNode(); Preferences projectNode= root.node(ProjectScope.SCOPE).node(project.getName()); // check if we need to notify the charset manager boolean hasResourcesSettings= getFile(project, ResourcesPlugin.PI_RESOURCES).exists(); // remove the preferences removeNode(projectNode); // notifies the CharsetManager if (hasResourcesSettings) preferencesChanged(project); } static void deleted(IResource resource) throws CoreException { switch (resource.getType()) { case IResource.FILE: deleted((IFile)resource); return; case IResource.FOLDER: deleted((IFolder)resource); return; case IResource.PROJECT: deleted((IProject)resource); return; } } /* * Return the preferences file for the given folder and qualifier. */ static IFile getFile(IFolder folder, String qualifier) { Assert.isLegal(folder.getName().equals(DEFAULT_PREFERENCES_DIRNAME)); return folder.getFile(new Path(qualifier).addFileExtension(PREFS_FILE_EXTENSION)); } /* * Return the preferences file for the given project and qualifier. */ static IFile getFile(IProject project, String qualifier) { return project.getFile(new Path(DEFAULT_PREFERENCES_DIRNAME).append(qualifier).addFileExtension(PREFS_FILE_EXTENSION)); } private static Properties loadProperties(IFile file) throws BackingStoreException { if (Policy.DEBUG_PREFERENCES) Policy.debug("Loading preferences from file: " + file.getFullPath()); //$NON-NLS-1$ Properties result= new Properties(); InputStream input= null; try { input= new BufferedInputStream(file.getContents(true)); result.load(input); } catch (CoreException e) { String message= NLS.bind(Messages.preferences_loadException, file.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } catch (IOException e) { String message= NLS.bind(Messages.preferences_loadException, file.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } finally { FileUtil.safeClose(input); } return result; } private static void preferencesChanged(IProject project) { Workspace workspace= ((Workspace)ResourcesPlugin.getWorkspace()); workspace.getCharsetManager().projectPreferencesChanged(project); workspace.getContentDescriptionManager().projectPreferencesChanged(project); } private static void read(ProjectPreferences node, IFile file) throws BackingStoreException, CoreException { if (file == null || !file.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Unable to determine preference file or file does not exist for node: " + node.absolutePath()); //$NON-NLS-1$ return; } Properties fromDisk= loadProperties(file); // no work to do if (fromDisk.isEmpty()) return; // create a new node to store the preferences in. IExportedPreferences myNode= (IExportedPreferences)ExportedPreferences.newRoot().node(node.absolutePath()); convertFromProperties((EclipsePreferences)myNode, fromDisk, false); //flag that we are currently reading, to avoid unnecessary writing boolean oldIsReading= node.isReading; node.isReading= true; try { Platform.getPreferencesService().applyPreferences(myNode); } finally { node.isReading= oldIsReading; } } static void removeNode(Preferences node) throws CoreException { String message= NLS.bind(Messages.preferences_removeNodeException, node.absolutePath()); try { node.removeNode(); } catch (BackingStoreException e) { IStatus status= new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e); throw new CoreException(status); } removeLoadedNodes(node); } static void clearNode(Preferences node) throws CoreException { // if the underlying properties file was deleted, clear the values and remove // it from the list of loaded nodes, keep the node as it might still be referenced try { clearAll(node); } catch (BackingStoreException e) { String message= NLS.bind(Messages.preferences_clearNodeException, node.absolutePath()); IStatus status= new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e); throw new CoreException(status); } removeLoadedNodes(node); } private static void clearAll(Preferences node) throws BackingStoreException { node.clear(); String[] names= node.childrenNames(); for (int i= 0; i < names.length; i++) { clearAll(node.node(names[i])); } } private static void removeLoadedNodes(Preferences node) { String path= node.absolutePath(); synchronized (loadedNodes) { for (Iterator i= loadedNodes.iterator(); i.hasNext();) { String key= (String)i.next(); if (key.startsWith(path)) i.remove(); } } } public static void updatePreferences(IFile file) throws CoreException { IPath path= file.getFullPath(); // if we made it this far we are inside /project/.settings and might // have a change to a preference file if (!PREFS_FILE_EXTENSION.equals(path.getFileExtension())) return; String project= path.segment(0); String qualifier= path.removeFileExtension().lastSegment(); Preferences root= Platform.getPreferencesService().getRootNode(); Preferences node= root.node(ProjectScope.SCOPE).node(project).node(qualifier); String message= null; try { message= NLS.bind(Messages.preferences_syncException, node.absolutePath()); if (!(node instanceof ProjectPreferences)) return; ProjectPreferences projectPrefs= (ProjectPreferences)node; if (projectPrefs.isWriting) return; read(projectPrefs, file); // Bug 108066: In case the node had existed before it was updated from // file, the read() operation marks it dirty. Override the dirty flag // since we know that the node is expected to be in sync with the file. projectPrefs.dirty= false; // make sure that we generate the appropriate resource change events // if encoding settings have changed if (ResourcesPlugin.PI_RESOURCES.equals(qualifier)) preferencesChanged(file.getProject()); } catch (BackingStoreException e) { IStatus status= new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e); throw new CoreException(status); } } /** * Default constructor. Should only be called by #createExecutableExtension. */ public ProjectPreferences() { super(null, null); } private ProjectPreferences(EclipsePreferences parent, String name) { super(parent, name); // cache the segment count String path= absolutePath(); segmentCount= getSegmentCount(path); if (segmentCount == 1) return; // cache the project name String projectName= getSegment(path, 1); if (projectName != null) project= ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); // cache the qualifier if (segmentCount > 2) qualifier= getSegment(path, 2); if (segmentCount != 2) return; // else segmentCount == 2 so we initialize the children if (initialized) return; try { synchronized (this) { String[] names= computeChildren(); for (int i= 0; i < names.length; i++) addChild(names[i], null); } } finally { initialized= true; } } /* * Figure out what the children of this node are based on the resources * that are in the workspace. */ private String[] computeChildren() { if (project == null) return EMPTY_STRING_ARRAY; IFolder folder= project.getFolder(DEFAULT_PREFERENCES_DIRNAME); if (!folder.exists()) return EMPTY_STRING_ARRAY; IResource[] members= null; try { members= folder.members(); } catch (CoreException e) { return EMPTY_STRING_ARRAY; } ArrayList result= new ArrayList(); for (int i= 0; i < members.length; i++) { IResource resource= members[i]; if (resource.getType() == IResource.FILE && PREFS_FILE_EXTENSION.equals(resource.getFullPath().getFileExtension())) result.add(resource.getFullPath().removeFileExtension().lastSegment()); } return (String[])result.toArray(EMPTY_STRING_ARRAY); } public void flush() throws BackingStoreException { if (isReading) return; isWriting= true; try { super.flush(); } finally { isWriting= false; } } private IFile getFile() { if (file == null) { if (project == null || qualifier == null) return null; file= getFile(project, qualifier); } return file; } /* * Return the node at which these preferences are loaded/saved. */ protected IEclipsePreferences getLoadLevel() { if (loadLevel == null) { if (project == null || qualifier == null) return null; // Make it relative to this node rather than navigating to it from the root. // Walk backwards up the tree starting at this node. // This is important to avoid a chicken/egg thing on startup. EclipsePreferences node= this; for (int i= 3; i < segmentCount; i++) node= (EclipsePreferences)node.parent(); loadLevel= node; } return loadLevel; } /* * Calculate and return the file system location for this preference node. * Use the absolute path of the node to find out the project name so * we can get its location on disk. * * NOTE: we cannot cache the location since it may change over the course * of the project life-cycle. */ protected IPath getLocation() { if (project == null || qualifier == null) return null; IPath path= project.getLocation(); return computeLocation(path, qualifier); } protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) { return new ProjectPreferences(nodeParent, nodeName); } protected boolean isAlreadyLoaded(IEclipsePreferences node) { return loadedNodes.contains(node.absolutePath()); } protected boolean isAlreadyLoaded(String path) { return loadedNodes.contains(path); } protected void load() throws BackingStoreException { IFile localFile= getFile(); if (localFile == null || !localFile.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Unable to determine preference file or file does not exist for node: " + absolutePath()); //$NON-NLS-1$ return; } if (Policy.DEBUG_PREFERENCES) Policy.debug("Loading preferences from file: " + localFile.getFullPath()); //$NON-NLS-1$ Properties fromDisk= new Properties(); InputStream input= null; try { input= new BufferedInputStream(localFile.getContents(true)); fromDisk.load(input); } catch (CoreException e) { String message= NLS.bind(Messages.preferences_loadException, localFile.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } catch (IOException e) { String message= NLS.bind(Messages.preferences_loadException, localFile.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } finally { FileUtil.safeClose(input); } convertFromProperties(this, fromDisk, true); } protected void loaded() { loadedNodes.add(absolutePath()); } /* (non-Javadoc) * @see org.eclipse.core.internal.preferences.EclipsePreferences#nodeExists(java.lang.String) * * If we are at the /project node and we are checking for the existence of a child, we * want special behaviour. If the child is a single segment name, then we want to * return true if the node exists OR if a project with that name exists in the workspace. */ public boolean nodeExists(String path) throws BackingStoreException { if (segmentCount != 1) return super.nodeExists(path); if (path.length() == 0) return super.nodeExists(path); if (path.charAt(0) == IPath.SEPARATOR) return super.nodeExists(path); if (path.indexOf(IPath.SEPARATOR) != -1) return super.nodeExists(path); // if we are checking existance of a single segment child of /project, base the answer on // whether or not it exists in the workspace. return ResourcesPlugin.getWorkspace().getRoot().getProject(path).exists() || super.nodeExists(path); } protected void save() throws BackingStoreException { final IFile fileInWorkspace= getFile(); if (fileInWorkspace == null) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Not saving preferences since there is no file for node: " + absolutePath()); //$NON-NLS-1$ return; } Properties table= convertToProperties(new SortedProperties(), ""); //$NON-NLS-1$ IWorkspace workspace= ResourcesPlugin.getWorkspace(); IResourceRuleFactory factory= workspace.getRuleFactory(); try { if (table.isEmpty()) { IWorkspaceRunnable operation= new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { // nothing to save. delete existing file if one exists. if (fileInWorkspace.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Deleting preference file: " + fileInWorkspace.getFullPath()); //$NON-NLS-1$ if (fileInWorkspace.isReadOnly()) { IStatus status= fileInWorkspace.getWorkspace().validateEdit(new IFile[] { fileInWorkspace }, IWorkspace.VALIDATE_PROMPT); if (!status.isOK()) throw new CoreException(status); } try { fileInWorkspace.delete(true, null); } catch (CoreException e) { String message= NLS.bind(Messages.preferences_deleteException, fileInWorkspace.getFullPath()); log(new Status(IStatus.WARNING, ResourcesPlugin.PI_RESOURCES, IStatus.WARNING, message, null)); } } } }; ISchedulingRule rule= factory.deleteRule(fileInWorkspace); try { ResourcesPlugin.getWorkspace().run(operation, rule, IResource.NONE, null); } catch (OperationCanceledException e) { throw new BackingStoreException(Messages.preferences_operationCanceled); } return; } table.put(VERSION_KEY, VERSION_VALUE); ByteArrayOutputStream output= new ByteArrayOutputStream(); try { table.store(output, null); } catch (IOException e) { String message= NLS.bind(Messages.preferences_saveProblems, absolutePath()); log(new Status(IStatus.ERROR, Platform.PI_RUNTIME, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } finally { try { output.close(); } catch (IOException e) { // ignore } } final InputStream input= new BufferedInputStream(new ByteArrayInputStream(output.toByteArray())); IWorkspaceRunnable operation= new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { if (fileInWorkspace.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Setting preference file contents for: " + fileInWorkspace.getFullPath()); //$NON-NLS-1$ if (fileInWorkspace.isReadOnly()) { IStatus status= fileInWorkspace.getWorkspace().validateEdit(new IFile[] { fileInWorkspace }, IWorkspace.VALIDATE_PROMPT); if (!status.isOK()) throw new CoreException(status); } // set the contents fileInWorkspace.setContents(input, IResource.KEEP_HISTORY, null); } else { // create the file IFolder folder= (IFolder)fileInWorkspace.getParent(); if (!folder.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Creating parent preference directory: " + folder.getFullPath()); //$NON-NLS-1$ folder.create(IResource.NONE, true, null); } if (Policy.DEBUG_PREFERENCES) Policy.debug("Creating preference file: " + fileInWorkspace.getLocation()); //$NON-NLS-1$ fileInWorkspace.create(input, IResource.NONE, null); } } }; //don't bother with scheduling rules if we are already inside an operation try { if (((Workspace)workspace).getWorkManager().isLockAlreadyAcquired()) { operation.run(null); } else { // we might: create the .settings folder, create the file, or modify the file. ISchedulingRule rule= MultiRule.combine(factory.createRule(fileInWorkspace.getParent()), factory.modifyRule(fileInWorkspace)); workspace.run(operation, rule, IResource.NONE, null); } } catch (OperationCanceledException e) { throw new BackingStoreException(Messages.preferences_operationCanceled); } } catch (CoreException e) { String message= NLS.bind(Messages.preferences_saveProblems, fileInWorkspace.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message, e)); throw new BackingStoreException(message); } } }