/******************************************************************************* * Copyright (c) 2004, 2011 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 * James Blackburn (Broadcom Corp.) - ongoing development *******************************************************************************/ 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 { private static final long serialVersionUID = 1L; /* (non-Javadoc) * @see java.util.Hashtable#keys() */ public synchronized Enumeration<Object> keys() { TreeSet<Object> set = new TreeSet<Object>(); for (Enumeration<Object> e = super.keys(); e.hasMoreElements();) set.add(e.nextElement()); return Collections.enumeration(set); } /* (non-Javadoc) * @see java.util.Hashtable#entrySet() */ public Set<Map.Entry<Object, Object>> entrySet() { TreeSet<Map.Entry<Object, Object>> set = new TreeSet<Map.Entry<Object, Object>>(new Comparator<Map.Entry<Object, Object>>() { public int compare(Map.Entry<Object, Object> e1, Map.Entry<Object, Object> e2) { String s1 = (String) e1.getKey(); String s2 = (String) e2.getKey(); return s1.compareTo(s2); } }); for (Iterator<Map.Entry<Object, Object>> i = super.entrySet().iterator(); i.hasNext();) set.add(i.next()); return set; } } static final String PREFS_REGULAR_QUALIFIER = ResourcesPlugin.PI_RESOURCES; static final String PREFS_DERIVED_QUALIFIER = PREFS_REGULAR_QUALIFIER + ".derived"; //$NON-NLS-1$ /** * Cache which nodes have been loaded from disk */ protected static Set<String> loadedNodes = Collections.synchronizedSet(new HashSet<String>()); 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(PREFS_REGULAR_QUALIFIER) || qualifier.equals(PREFS_DERIVED_QUALIFIER)) 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, PREFS_REGULAR_QUALIFIER).exists() || getFile(folder, PREFS_DERIVED_QUALIFIER).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, PREFS_REGULAR_QUALIFIER).exists() || getFile(project, PREFS_DERIVED_QUALIFIER).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<String> i = loadedNodes.iterator(); i.hasNext();) { String key = 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 (PREFS_REGULAR_QUALIFIER.equals(qualifier) || PREFS_DERIVED_QUALIFIER.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); initialize(); } /* * 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<String> result = new ArrayList<String>(); 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 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 String internalPut(String key, String newValue) { if ((segmentCount == 3) && PREFS_REGULAR_QUALIFIER.equals(qualifier) && (project != null)) { if (ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS.equals(key)) { Workspace workspace = ((Workspace) ResourcesPlugin.getWorkspace()); if (Boolean.parseBoolean(newValue)) workspace.getCharsetManager().splitEncodingPreferences(project); else workspace.getCharsetManager().mergeEncodingPreferences(project); } } return super.internalPut(key, newValue); } protected void initialize() { if (segmentCount != 2) return; // if already initialized, then skip this initialization if (initialized) return; // initialize the children only if project is opened if (project.isOpen()) { try { synchronized (this) { String[] names = computeChildren(); for (int i = 0; i < names.length; i++) addChild(names[i], null); } } finally { // mark as initialized so that subsequent project opening will not initialize preferences again initialized = true; } } } 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; } // Before loading the preferences, try to initialize parent node (/project/<projectName>) // see bug 335591 ((ProjectPreferences) parent).initialize(); 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); 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); } public void remove(String key) { super.remove(key); if ((segmentCount == 3) && PREFS_REGULAR_QUALIFIER.equals(qualifier) && (project != null)) { if (ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS.equals(key)) { Workspace workspace = ((Workspace) ResourcesPlugin.getWorkspace()); workspace.getCharsetManager().mergeEncodingPreferences(project); } } } 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); output.close(); } 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 { FileUtil.safeClose(output); } final InputStream input = new BufferedInputStream(new ByteArrayInputStream(output.toByteArray())); final String finalQualifier = qualifier; 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); } if (PREFS_DERIVED_QUALIFIER.equals(finalQualifier)) fileInWorkspace.setDerived(true, 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, modify the file, or set derived flag for the file. ISchedulingRule rule = MultiRule.combine(new ISchedulingRule[] {factory.createRule(fileInWorkspace.getParent()), factory.modifyRule(fileInWorkspace), factory.derivedRule(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); } } }