blob: fce572580938746ad7d59cbbc08c85bb6f8e9771 [file] [log] [blame]
/*******************************************************************************
* 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
*/
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
*/
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();
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.
*/
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.
*/
@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.lineSeparator();
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);
}
}
}