/*******************************************************************************
 * 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);
		}
	}
}
