Copyright (c) 2004, 2015 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation Markus Schorn (Wind River) - [108066] Project prefs marked dirty on read James Blackburn (Broadcom Corp.) - ongoing development Lars Vogel - Bug 473427, 483529
/******************************************************************************* * Copyright (c) 2004, 2015 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Markus Schorn (Wind River) - [108066] Project prefs marked dirty on read * James Blackburn (Broadcom Corp.) - ongoing development * Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427, 483529 *******************************************************************************/
package org.eclipse.core.internal.resources; import java.io.*; import java.text.MessageFormat; import java.util.*; import org.eclipse.core.internal.preferences.*; 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
/** * Represents a node in the Eclipse preference hierarchy which stores preference * values for projects. * * @since 3.0 */
public class ProjectPreferences extends EclipsePreferences { 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
/** * 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.
/** * 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.
/** * 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(); try ( InputStream input = new BufferedInputStream(file.getContents(true)); ) { result.load(input); } catch (CoreException e) { if (e.getStatus().getCode() == IResourceStatus.RESOURCE_NOT_FOUND) { if (Policy.DEBUG_PREFERENCES) Policy.debug(MessageFormat.format("Preference file {0} does not exist.", file.getFullPath())); //$NON-NLS-1$ } else { 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); } 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 (String name2 : names) { clearAll(node.node(name2)); } } 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.
/** * 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); } @Override public String[] childrenNames() throws BackingStoreException { // illegal state if this node has been removed checkRemoved(); initialize(); silentLoad(); return super.childrenNames(); } @Override public void clear() { // illegal state if this node has been removed checkRemoved(); silentLoad(); super.clear(); } /* * 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<>(); for (IResource resource : members) { if (resource.getType() == IResource.FILE && PREFS_FILE_EXTENSION.equals(resource.getFullPath().getFileExtension())) result.add(resource.getFullPath().removeFileExtension().lastSegment()); } return result.toArray(EMPTY_STRING_ARRAY); } @Override public void flush() throws BackingStoreException { if (isReading) return; isWriting = true; try { // call the internal method because we don't want to be synchronized, we will do that ourselves later. IEclipsePreferences toFlush = super.internalFlush(); //if we aren't at the right level, then flush the appropriate node if (toFlush != null) toFlush.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. */ @Override 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. */ @Override protected IPath getLocation() { if (project == null || qualifier == null) return null; IPath path = project.getLocation(); return computeLocation(path, qualifier); } @Override protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) { return new ProjectPreferences(nodeParent, nodeName); } @Override protected String internalGet(String key) { // throw NPE if key is null if (key == null) throw new NullPointerException(); // illegal state if this node has been removed checkRemoved(); silentLoad(); return super.internalGet(key); } @Override protected String internalPut(String key, String newValue) { // illegal state if this node has been removed checkRemoved(); silentLoad(); if ((segmentCount == 3) && PREFS_REGULAR_QUALIFIER.equals(qualifier) && (project != null)) { if (ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS.equals(key)) { CharsetManager charsetManager = ((Workspace) ResourcesPlugin.getWorkspace()).getCharsetManager(); if (Boolean.parseBoolean(newValue)) charsetManager.splitEncodingPreferences(project); else charsetManager.mergeEncodingPreferences(project); } } return super.internalPut(key, newValue); } private 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) { List<String> addedNames = Arrays.asList(internalChildNames()); String[] names = computeChildren(); // add names only for nodes that were not added previously for (String name : names) { if (!addedNames.contains(name)) { addChild(name, null); } } } } finally { // mark as initialized so that subsequent project opening will not initialize preferences again initialized = true; } } } @Override protected boolean isAlreadyLoaded(IEclipsePreferences node) { return loadedNodes.contains(node.absolutePath()); } @Override public String[] keys() { // illegal state if this node has been removed checkRemoved(); silentLoad(); return super.keys(); } @Override protected void load() throws BackingStoreException { load(true); } private void load(boolean reportProblems) 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(); try ( InputStream input = new BufferedInputStream(localFile.getContents(true)); ) { fromDisk.load(input); convertFromProperties(this, fromDisk, true); loadedNodes.add(absolutePath()); } catch (CoreException e) { if (e.getStatus().getCode() == IResourceStatus.RESOURCE_NOT_FOUND) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Preference file does not exist for node: " + absolutePath()); //$NON-NLS-1$ return; } if (reportProblems) { 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) { if (reportProblems) { 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); } } }
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.
/** * 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. */
@Override public boolean nodeExists(String path) throws BackingStoreException { // short circuit for checking this node if (path.length() == 0) return !removed; // illegal state if this node has been removed. // do this AFTER checking for the empty string. checkRemoved(); initialize(); silentLoad(); 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); } @Override public void remove(String key) { // illegal state if this node has been removed checkRemoved(); silentLoad(); super.remove(key); if ((segmentCount == 3) && PREFS_REGULAR_QUALIFIER.equals(qualifier) && (project != null)) { if (ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS.equals(key)) { CharsetManager charsetManager = ((Workspace) ResourcesPlugin.getWorkspace()).getCharsetManager(); if (ResourcesPlugin.DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS) charsetManager.splitEncodingPreferences(project); else charsetManager.mergeEncodingPreferences(project); } } } @Override 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; } final String finalQualifier = qualifier; final BackingStoreException[] bse = new BackingStoreException[1]; try { ICoreRunnable operation = monitor -> { try { Properties table = convertToProperties(new SortedProperties(), ""); //$NON-NLS-1$ // nothing to save. delete existing file if one exists. if (table.isEmpty()) { if (fileInWorkspace.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Deleting preference file: " + fileInWorkspace.getFullPath()); //$NON-NLS-1$ if (fileInWorkspace.isReadOnly()) { IStatus status1 = fileInWorkspace.getWorkspace().validateEdit(new IFile[] {fileInWorkspace}, IWorkspace.VALIDATE_PROMPT); if (!status1.isOK()) throw new CoreException(status1); } try { fileInWorkspace.delete(true, null); } catch (CoreException e1) { String message1 = NLS.bind(Messages.preferences_deleteException, fileInWorkspace.getFullPath()); log(new Status(IStatus.WARNING, ResourcesPlugin.PI_RESOURCES, IStatus.WARNING, message1, null)); } } return; } table.put(VERSION_KEY, VERSION_VALUE); // print the table to a string and remove the timestamp that Properties#store always adds String s = removeTimestampFromTable(table); String systemLineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$ String fileLineSeparator = FileUtil.getLineSeparator(fileInWorkspace); if (!systemLineSeparator.equals(fileLineSeparator)) s = s.replaceAll(systemLineSeparator, fileLineSeparator); InputStream input = new BufferedInputStream(new ByteArrayInputStream(s.getBytes("UTF-8"))); //$NON-NLS-1$ // make sure that preference folder and file are in sync fileInWorkspace.getParent().refreshLocal(IResource.DEPTH_ZERO, null); fileInWorkspace.refreshLocal(IResource.DEPTH_ZERO, null); if (fileInWorkspace.exists()) { if (Policy.DEBUG_PREFERENCES) Policy.debug("Setting preference file contents for: " + fileInWorkspace.getFullPath()); //$NON-NLS-1$ if (fileInWorkspace.isReadOnly()) { IStatus status2 = fileInWorkspace.getWorkspace().validateEdit(new IFile[] {fileInWorkspace}, IWorkspace.VALIDATE_PROMPT); if (!status2.isOK()) { input.close(); throw new CoreException(status2); } } // 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); } catch (BackingStoreException e2) { bse[0] = e2; } catch (IOException e3) { String message2 = NLS.bind(Messages.preferences_saveProblems, fileInWorkspace.getFullPath()); log(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, IStatus.ERROR, message2, e3)); bse[0] = new BackingStoreException(message2); } }; //don't bother with scheduling rules if we are already inside an operation try { IWorkspace workspace = ResourcesPlugin.getWorkspace(); if (((Workspace) workspace).getWorkManager().isLockAlreadyAcquired()) { operation.run(null); } else { IResourceRuleFactory factory = workspace.getRuleFactory(); // we might: delete the file, create the .settings folder, create the file, modify the file, or set derived flag for the file. ISchedulingRule rule = MultiRule.combine(new ISchedulingRule[] {factory.deleteRule(fileInWorkspace), factory.createRule(fileInWorkspace.getParent()), factory.modifyRule(fileInWorkspace), factory.derivedRule(fileInWorkspace)}); workspace.run(operation, rule, IResource.NONE, null); if (bse[0] != null) throw bse[0]; } } 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); } } private void silentLoad() { ProjectPreferences node = (ProjectPreferences) getLoadLevel(); if (node == null) return; if (isAlreadyLoaded(node) || node.isLoading()) return; try { node.setLoading(true); node.load(false); } catch (BackingStoreException e) { // will not happen, all exceptions are swallowed by load(false) } finally { node.setLoading(false); } } }