/*******************************************************************************
 * Copyright (c) 2000, 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
 *     Martin Oberhuber (Wind River) - [210664] descriptionChanged(): ignore LF style
 *     Martin Oberhuber (Wind River) - [233939] findFilesForLocation() with symlinks
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *******************************************************************************/
package org.eclipse.core.internal.localstore;

import java.io.*;
import java.net.URI;
import java.util.*;
import org.eclipse.core.filesystem.*;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.internal.resources.*;
import org.eclipse.core.internal.resources.File;
import org.eclipse.core.internal.utils.*;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
import org.eclipse.osgi.util.NLS;
import org.xml.sax.InputSource;

/**
 * Manages the synchronization between the workspace's view and the file system.  
 */
public class FileSystemResourceManager implements ICoreConstants, IManager, Preferences.IPropertyChangeListener {

	/**
	 * The history store is initialized lazily - always use the accessor method
	 */
	protected IHistoryStore _historyStore;
	protected Workspace workspace;

	private volatile boolean lightweightAutoRefreshEnabled;

	public FileSystemResourceManager(Workspace workspace) {
		this.workspace = workspace;
	}

	/**
	 * Returns the workspace paths of all resources that may correspond to
	 * the given file system location.  Returns an empty ArrayList if there are no 
	 * such paths.  This method does not consider whether resources actually 
	 * exist at the given locations.
	 * <p>
	 * The workspace paths of {@link IResource#HIDDEN} project and resources
	 * located in {@link IResource#HIDDEN} projects won't be added to the result.
	 * </p>
	 * 
	 */
	protected ArrayList<IPath> allPathsForLocation(URI inputLocation) {
		URI canonicalLocation = FileUtil.canonicalURI(inputLocation);
		// First, try the canonical version of the inputLocation.
		// If the inputLocation is different from the canonical version, it will be tried second
		ArrayList<IPath> results = allPathsForLocationNonCanonical(canonicalLocation);
		if(results.size()==0 && canonicalLocation!=inputLocation) {
			results = allPathsForLocationNonCanonical(inputLocation);
		}
		return results;
	}
	
	private ArrayList<IPath> allPathsForLocationNonCanonical(URI inputLocation) {
		URI location = inputLocation;
		final boolean isFileLocation = EFS.SCHEME_FILE.equals(inputLocation.getScheme());
		final IWorkspaceRoot root = getWorkspace().getRoot();
		final ArrayList<IPath> results = new ArrayList<IPath>();
		if (URIUtil.equals(location, root.getLocationURI())) {
			//there can only be one resource at the workspace root's location
			results.add(Path.ROOT);
			return results;
		}
		IProject[] projects = root.getProjects(IContainer.INCLUDE_HIDDEN);
		for (int i = 0; i < projects.length; i++) {
			IProject project = projects[i];
			//check the project location
			URI testLocation = project.getLocationURI();
			if (testLocation == null)
				continue;
			boolean usingAnotherScheme = !inputLocation.getScheme().equals(testLocation.getScheme());
			// if we are looking for file: locations try to get a file: location for this project
			if (isFileLocation && !EFS.SCHEME_FILE.equals(testLocation.getScheme()))
				testLocation = getFileURI(testLocation);
			if (testLocation == null)
				continue;
			URI relative = testLocation.relativize(location);
			if (!relative.isAbsolute() && !relative.equals(testLocation)) {
				IPath suffix = new Path(relative.getPath());
				results.add(project.getFullPath().append(suffix));
			}
			if (usingAnotherScheme) {
				// if a different scheme is used, we can't use the AliasManager, since the manager
				// map is stored using the EFS scheme, and not necessarily the SCHEME_FILE 
				ProjectDescription description = ((Project) project).internalGetDescription();
				if (description == null)
					continue;
				HashMap<IPath,LinkDescription> links = description.getLinks();
				if (links == null)
					continue;
				for (LinkDescription link : links.values()) {
					IResource resource = project.findMember(link.getProjectRelativePath());
					IPathVariableManager pathMan = resource == null ? project.getPathVariableManager() : resource.getPathVariableManager();
					testLocation = pathMan.resolveURI(link.getLocationURI());
					// if we are looking for file: locations try to get a file: location for this link
					if (isFileLocation && !EFS.SCHEME_FILE.equals(testLocation.getScheme()))
						testLocation = getFileURI(testLocation);
					if (testLocation == null)
						continue;
					relative = testLocation.relativize(location);
					if (!relative.isAbsolute() && !relative.equals(testLocation)) {
						IPath suffix = new Path(relative.getPath());
						results.add(project.getFullPath().append(link.getProjectRelativePath()).append(suffix));
					}
				}
			}
		}
		try {
			findLinkedResourcesPaths(inputLocation, results);
		} catch (CoreException e) {
			Policy.log(e);
		}
		return results;
	}

	/**
	 * Asynchronously auto-refresh the requested resource if {@link ResourcesPlugin#PREF_LIGHTWEIGHT_AUTO_REFRESH} is enabled.
	 * @param target
	 */
	private void asyncRefresh(IResource target) {
		if (lightweightAutoRefreshEnabled)
			workspace.getRefreshManager().refresh(target);
	}

	private void findLinkedResourcesPaths(URI inputLocation, final ArrayList<IPath> results) throws CoreException {
		IPath suffix = null;
		IFileStore fileStore = EFS.getStore(inputLocation);
		while (fileStore != null) {
			IResource[] resources = workspace.getAliasManager().findResources(fileStore);
			for (int i = 0; i < resources.length; i++) {
				if (resources[i].isLinked()) {
					IPath path = resources[i].getFullPath();
					if (suffix != null)
						path = path.append(suffix);
					if (!results.contains(path))
						results.add(path);
				}
			}
			if (suffix == null)
				suffix = Path.fromPortableString(fileStore.getName());
			else
				suffix = Path.fromPortableString(fileStore.getName()).append(suffix);
			fileStore = fileStore.getParent();
		}
	}

	/** 
	 * Tries to obtain a file URI for the given URI. Returns <code>null</code> if the file system associated
	 * to the URI scheme does not map to the local file system. 
	 * @param locationURI the URI to convert
	 * @return a file URI or <code>null</code>
	 */
	private URI getFileURI(URI locationURI) {
		try {
			IFileStore testLocationStore = EFS.getStore(locationURI);
			java.io.File storeAsFile = testLocationStore.toLocalFile(EFS.NONE, null);
			if (storeAsFile != null)
				return URIUtil.toURI(storeAsFile.getAbsolutePath());
		} catch (CoreException e) {
			// we don't know such file system or some other failure, just return null
		}
		return null;
	}

	/**
	 * Returns all resources that correspond to the given file system location,
	 * including resources under linked resources. Returns an empty array if
	 * there are no corresponding resources.
	 * <p>
	 * If the {@link IContainer#INCLUDE_TEAM_PRIVATE_MEMBERS} flag is specified
	 * in the member flags, team private members will be included along with the
	 * others. If the {@link IContainer#INCLUDE_TEAM_PRIVATE_MEMBERS} flag is
	 * not specified (recommended), the result will omit any team private member
	 * resources.
	 * </p>
	 * <p>
	 * If the {@link IContainer#INCLUDE_HIDDEN} flag is specified in the member
	 * flags, hidden members will be included along with the others. If the
	 * {@link IContainer#INCLUDE_HIDDEN} flag is not specified (recommended),
	 * the result will omit any hidden member resources.
	 * </p>
	 * <p>
	 * The result will also omit resources that are explicitly excluded 
	 * from the workspace according to existing resource filters.
	 * </p>
	 * 
	 * @param location
	 *        the file system location
	 * @param files
	 *        resources that may exist below the project level can be either
	 *        files or folders. If this parameter is true, files will be
	 *        returned, otherwise containers will be returned.
	 * @param memberFlags
	 *        bit-wise or of member flag constants (
	 *        {@link IContainer#INCLUDE_TEAM_PRIVATE_MEMBERS} and
	 *        {@link IContainer#INCLUDE_HIDDEN}) indicating which members are of
	 *        interest
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	public IResource[] allResourcesFor(URI location, boolean files, int memberFlags) {
		ArrayList result = allPathsForLocation(location);
		int count = 0;
		for (int i = 0, imax = result.size(); i < imax; i++) {
			//replace the path in the list with the appropriate resource type
			IResource resource = resourceFor((IPath)result.get(i), files);

			if (resource == null || ((Resource) resource).isFiltered() || (((memberFlags & IContainer.INCLUDE_HIDDEN) == 0) && resource.isHidden(IResource.CHECK_ANCESTORS)) 
					|| (((memberFlags & IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS) == 0) && resource.isTeamPrivateMember(IResource.CHECK_ANCESTORS)))
				resource = null;

			result.set(i, resource);
			//count actual resources - some paths won't have a corresponding resource
			if (resource != null)
				count++;
		}
		//convert to array and remove null elements
		IResource[] toReturn = files ? (IResource[]) new IFile[count] : (IResource[]) new IContainer[count];
		count = 0;
		for (Iterator it = result.iterator(); it.hasNext();) {
			IResource resource = (IResource) it.next();
			if (resource != null)
				toReturn[count++] = resource;
		}
		return toReturn;
	}

	/* (non-javadoc)
	 * @see IResource.getResourceAttributes
	 */
	public ResourceAttributes attributes(IResource resource) {
		IFileStore store = getStore(resource);
		IFileInfo fileInfo = store.fetchInfo();
		if (!fileInfo.exists())
			return null;
		return FileUtil.fileInfoToAttributes(fileInfo);
	}

	/**
	 * Returns a container for the given file system location or null if there
	 * is no mapping for this path. If the path has only one segment, then an 
	 * <code>IProject</code> is returned.  Otherwise, the returned object
	 * is a <code>IFolder</code>.  This method does NOT check the existence
	 * of a folder in the given location. Location cannot be null.
	 * <p>
	 * The result will also omit resources that are explicitly excluded 
	 * from the workspace according to existing resource filters. If all resources 
	 * are omitted, the result may be null.
	 * </p>
	 */
	public IContainer containerForLocation(IPath location) {
		return (IContainer) resourceForLocation(location, false); 
	}

	/**
	 * Returns the resource corresponding to the given location.  The
	 * "files" parameter is used for paths of two or more segments.  If true,
	 * a file is returned, otherwise a folder is returned.  Returns null if files is true
	 * and the path is not of sufficient length. Also returns null if the resource is 
	 * filtered out by resource filters
	 */
	private IResource resourceForLocation(IPath location, boolean files) {
		if (workspace.getRoot().getLocation().equals(location)) {
			if (!files)
				return resourceFor(Path.ROOT, false);
			return null;
		}
		IProject[] projects = getWorkspace().getRoot().getProjects(IContainer.INCLUDE_HIDDEN);
		for (int i = 0; i < projects.length; i++) {
			IProject project = projects[i];
			IPath projectLocation = project.getLocation();
			if (projectLocation != null && projectLocation.isPrefixOf(location)) {
				int segmentsToRemove = projectLocation.segmentCount();
				IPath path = project.getFullPath().append(location.removeFirstSegments(segmentsToRemove));
				IResource resource = resourceFor(path, files);
				if (resource != null && !((Resource)resource).isFiltered())
					return resource;
			}
		}
		return null;
	}

	public void copy(IResource target, IResource destination, int updateFlags, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		try {
			int totalWork = ((Resource) target).countResources(IResource.DEPTH_INFINITE, false);
			String title = NLS.bind(Messages.localstore_copying, target.getFullPath());
			monitor.beginTask(title, totalWork);
			IFileStore destinationStore = getStore(destination);
			if (destinationStore.fetchInfo().exists()) {
				String message = NLS.bind(Messages.localstore_resourceExists, destination.getFullPath());
				throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, destination.getFullPath(), message, null);
			}
			getHistoryStore().copyHistory(target, destination, false);
			CopyVisitor visitor = new CopyVisitor(target, destination, updateFlags, monitor);
			UnifiedTree tree = new UnifiedTree(target);
			tree.accept(visitor, IResource.DEPTH_INFINITE);
			IStatus status = visitor.getStatus();
			if (!status.isOK())
				throw new ResourceException(status);
		} finally {
			monitor.done();
		}
	}

	public void delete(IResource target, int flags, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		try {
			Resource resource = (Resource) target;
			final int deleteWork = resource.countResources(IResource.DEPTH_INFINITE, false) * 2;
			boolean force = (flags & IResource.FORCE) != 0;
			int refreshWork = 0;
			if (!force)
				refreshWork = Math.min(deleteWork, 100);
			String title = NLS.bind(Messages.localstore_deleting, resource.getFullPath());
			monitor.beginTask(title, deleteWork + refreshWork);
			monitor.subTask(""); //$NON-NLS-1$
			MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.FAILED_DELETE_LOCAL, Messages.localstore_deleteProblem, null);
			List<Resource> skipList = null;
			UnifiedTree tree = new UnifiedTree(target);
			if (!force) {
				IProgressMonitor sub = Policy.subMonitorFor(monitor, refreshWork);
				sub.beginTask("", 1000); //$NON-NLS-1$
				try {
					CollectSyncStatusVisitor refreshVisitor = new CollectSyncStatusVisitor(Messages.localstore_deleteProblem, sub);
					refreshVisitor.setIgnoreLocalDeletions(true);
					tree.accept(refreshVisitor, IResource.DEPTH_INFINITE);
					status.merge(refreshVisitor.getSyncStatus());
					skipList = refreshVisitor.getAffectedResources();
				} finally {
					sub.done();
				}
			}
			DeleteVisitor deleteVisitor = new DeleteVisitor(skipList, flags, monitor, deleteWork);
			tree.accept(deleteVisitor, IResource.DEPTH_INFINITE);
			status.merge(deleteVisitor.getStatus());
			if (!status.isOK())
				throw new ResourceException(status);
		} finally {
			monitor.done();
		}
	}

	/**
	 * Returns true if the description on disk is different from the given byte array,
	 * and false otherwise.
	 * Since org.eclipse.core.resources 3.4.1 differences in line endings (CR, LF, CRLF) 
	 * are not considered.
	 */
	private boolean descriptionChanged(IFile descriptionFile, byte[] newContents) {
		InputStream oldStream = null;
		try {
			//buffer size: twice the description length, but maximum 8KB
			int bufsize = newContents.length > 4096 ? 8192 : newContents.length * 2;
			oldStream = new BufferedInputStream(descriptionFile.getContents(true), bufsize);
			InputStream newStream = new ByteArrayInputStream(newContents);
			//compare streams char by char, ignoring line endings
			int newChar = newStream.read();
			int oldChar = oldStream.read();
			while (newChar >= 0 && oldChar >= 0) {
				if (newChar == oldChar) {
					//streams are the same
					newChar = newStream.read();
					oldChar = oldStream.read();
				} else if ((newChar == '\r' || newChar == '\n') && (oldChar == '\r' || oldChar == '\n')) {
					//got a difference, but both sides are newlines: read over newlines
					while (newChar == '\r' || newChar == '\n')
						newChar = newStream.read();
					while (oldChar == '\r' || oldChar == '\n')
						oldChar = oldStream.read();
				} else {
					//streams are different
					return true;
				}
			}
			//test for excess data in one stream
			if (newChar >= 0 || oldChar >= 0)
				return true;
			return false;
		} catch (Exception e) {
			Policy.log(e);
			//if we failed to compare, just write the new contents
		} finally {
			FileUtil.safeClose(oldStream);
		}
		return true;
	}

	/**
	 * @deprecated
	 */
	public int doGetEncoding(IFileStore store) throws CoreException {
		InputStream input = null;
		try {
			input = store.openInputStream(EFS.NONE, null);
			int first = input.read();
			int second = input.read();
			if (first == -1 || second == -1)
				return IFile.ENCODING_UNKNOWN;
			first &= 0xFF;//converts unsigned byte to int
			second &= 0xFF;
			//look for the UTF-16 Byte Order Mark (BOM)
			if (first == 0xFE && second == 0xFF)
				return IFile.ENCODING_UTF_16BE;
			if (first == 0xFF && second == 0xFE)
				return IFile.ENCODING_UTF_16LE;
			int third = (input.read() & 0xFF);
			if (third == -1)
				return IFile.ENCODING_UNKNOWN;
			//look for the UTF-8 BOM
			if (first == 0xEF && second == 0xBB && third == 0xBF)
				return IFile.ENCODING_UTF_8;
			return IFile.ENCODING_UNKNOWN;
		} catch (IOException e) {
			String message = NLS.bind(Messages.localstore_couldNotRead, store.toString());
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, null, message, e);
		} finally {
			FileUtil.safeClose(input);
		}
	}

	/**
	 * Optimized sync check for files.  Returns true if the file exists and is in sync, and false
	 * otherwise.  The intent is to let the default implementation handle the complex
	 * cases like gender change, case variants, etc.
	 */
	public boolean fastIsSynchronized(File target) {
		ResourceInfo info = target.getResourceInfo(false, false);
		if (target.exists(target.getFlags(info), true)) {
			IFileInfo fileInfo = getStore(target).fetchInfo();
			if (!fileInfo.isDirectory() && info.getLocalSyncInfo() == fileInfo.getLastModified())
				return true;
		}
		return false;
	}
	
	public boolean fastIsSynchronized(Folder target) {
		ResourceInfo info = target.getResourceInfo(false, false);
		if (target.exists(target.getFlags(info), true)) {
			IFileInfo fileInfo = getStore(target).fetchInfo();
			if (!fileInfo.exists() && info.getLocalSyncInfo() == fileInfo.getLastModified())
				return true;
		}
		return false;
	}

	/**
	 * Returns an IFile for the given file system location or null if there
	 * is no mapping for this path. This method does NOT check the existence
	 * of a file in the given location. Location cannot be null.
	 * <p>
	 * The result will also omit resources that are explicitly excluded 
	 * from the workspace according to existing resource filters. If all resources 
	 * are omitted, the result may be null.
	 * </p>
	 */
	public IFile fileForLocation(IPath location) {
		return (IFile) resourceForLocation(location, true);
	}

	/**
	 * @deprecated
	 */
	public int getEncoding(File target) throws CoreException {
		// thread safety: (the location can be null if the project for this file does not exist)
		IFileStore store = getStore(target);
		if (!store.fetchInfo().exists()) {
			String message = NLS.bind(Messages.localstore_fileNotFound, store.toString());
			throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, target.getFullPath(), message, null);
		}
		return doGetEncoding(store);
	}

	public IHistoryStore getHistoryStore() {
		if (_historyStore == null) {
			IPath location = getWorkspace().getMetaArea().getHistoryStoreLocation();
			location.toFile().mkdirs();
			_historyStore = ResourcesCompatibilityHelper.createHistoryStore(location, 256);
		}
		return _historyStore;
	}

	/** 
	 * Returns the real name of the resource on disk. Returns null if no local
	 * file exists by that name.  This is useful when dealing with
	 * case insensitive file systems.
	 */
	public String getLocalName(IFileStore target) {
		return target.fetchInfo().getName();
	}

	protected IPath getProjectDefaultLocation(IProject project) {
		return workspace.getRoot().getLocation().append(project.getFullPath());
	}

	/**
	 * Never returns null
	 * @param target
	 * @return The file store for this resource
	 */
	public IFileStore getStore(IResource target) {
		try {
			return getStoreRoot(target).createStore(target.getFullPath(), target);
		} catch (CoreException e) {
			//callers aren't expecting failure here, so return null file system
			return EFS.getNullFileSystem().getStore(target.getFullPath());
		}
	}

	/**
	 * Returns the file store root for the provided resource. Never returns null.
	 */
	private FileStoreRoot getStoreRoot(IResource target) {
		ResourceInfo info = workspace.getResourceInfo(target.getFullPath(), true, false);
		FileStoreRoot root;
		if (info != null) {
			root = info.getFileStoreRoot();
			if (root != null && root.isValid())
				return root;
			if (info.isSet(ICoreConstants.M_VIRTUAL)) {
				ProjectDescription description = ((Project) target.getProject()).internalGetDescription();
				if (description != null) {
					setLocation(target, info, description.getGroupLocationURI(target.getProjectRelativePath()));
					return info.getFileStoreRoot();
				}
				return info.getFileStoreRoot();
			}
			if (info.isSet(ICoreConstants.M_LINK)) {
				ProjectDescription description = ((Project) target.getProject()).internalGetDescription();
				if (description != null) {
					final URI linkLocation = description.getLinkLocationURI(target.getProjectRelativePath());
					//if we can't determine the link location, fall through to parent resource
					if (linkLocation != null) {
						setLocation(target, info, linkLocation);
						return info.getFileStoreRoot();
					}
				}
			}
		}
		final IContainer parent = target.getParent();
		if (parent == null) {
			//this is the root, so we know where this must be located
			//initialize root location
			info = workspace.getResourceInfo(Path.ROOT, false, true);
			final IWorkspaceRoot rootResource = workspace.getRoot();
			setLocation(rootResource, info, URIUtil.toURI(rootResource.getLocation()));
			return info.getFileStoreRoot();
		}
		root = getStoreRoot(parent);
		if (info != null)
			info.setFileStoreRoot(root);
		return root;
	}

	protected Workspace getWorkspace() {
		return workspace;
	}

	/**
	 * Returns whether the project has any local content on disk.
	 */
	public boolean hasSavedContent(IProject project) {
		return getStore(project).fetchInfo().exists();
	}

	/**
	 * Returns whether the project has a project description file on disk.
	 */
	public boolean hasSavedDescription(IProject project) {
		return getStore(project).getChild(IProjectDescription.DESCRIPTION_FILE_NAME).fetchInfo().exists();
	}

	/**
	 * Initializes the file store for a resource.
	 * 
	 * @param target The resource to initialize the file store for.
	 * @param location the File system location of this resource on disk
	 * @return The file store for the provided resource
	 */
	private IFileStore initializeStore(IResource target, URI location) throws CoreException {
		ResourceInfo info = ((Resource) target).getResourceInfo(false, true);
		setLocation(target, info, location);
		FileStoreRoot root = getStoreRoot(target);
		return root.createStore(target.getFullPath(), target);
	}

	/**
	 * The target must exist in the workspace.  This method must only ever
	 * be called from Project.writeDescription(), because that method ensures
	 * that the description isn't then immediately discovered as a new change.
	 * @return true if a new description was written, and false if it wasn't written
	 * because it was unchanged
	 */
	public boolean internalWrite(IProject target, IProjectDescription description, int updateFlags, boolean hasPublicChanges, boolean hasPrivateChanges) throws CoreException {
		//write the project's private description to the metadata area
		if (hasPrivateChanges)
			getWorkspace().getMetaArea().writePrivateDescription(target);
		if (!hasPublicChanges)
			return false;
		//can't do anything if there's no description
		if (description == null)
			return false;

		//write the model to a byte array
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			new ModelObjectWriter().write(description, out);
		} catch (IOException e) {
			String msg = NLS.bind(Messages.resources_writeMeta, target.getFullPath());
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, target.getFullPath(), msg, e);
		}
		byte[] newContents = out.toByteArray();

		//write the contents to the IFile that represents the description
		IFile descriptionFile = target.getFile(IProjectDescription.DESCRIPTION_FILE_NAME);
		if (!descriptionFile.exists())
			workspace.createResource(descriptionFile, false);
		else {
			//if the description has not changed, don't write anything
			if (!descriptionChanged(descriptionFile, newContents))
				return false;
		}
		ByteArrayInputStream in = new ByteArrayInputStream(newContents);
		IFileStore descriptionFileStore = ((Resource) descriptionFile).getStore();
		IFileInfo fileInfo = descriptionFileStore.fetchInfo();
		if (fileInfo.getAttribute(EFS.ATTRIBUTE_READ_ONLY)) {
			IStatus result = getWorkspace().validateEdit(new IFile[] {descriptionFile}, null);
			if (!result.isOK())
				throw new ResourceException(result);
			// re-read the file info in case the file attributes were modified	
			fileInfo = descriptionFileStore.fetchInfo();
		}
		//write the project description file (don't use API because scheduling rule might not match)
		write(descriptionFile, in, fileInfo, IResource.FORCE, false, Policy.monitorFor(null));
		workspace.getAliasManager().updateAliases(descriptionFile, getStore(descriptionFile), IResource.DEPTH_ZERO, Policy.monitorFor(null));

		//update the timestamp on the project as well so we know when it has
		//been changed from the outside
		long lastModified = ((Resource) descriptionFile).getResourceInfo(false, false).getLocalSyncInfo();
		ResourceInfo info = ((Resource) target).getResourceInfo(false, true);
		updateLocalSync(info, lastModified);

		//for backwards compatibility, ensure the old .prj file is deleted
		getWorkspace().getMetaArea().clearOldDescription(target);
		return true;
	}

	/**
	 * Returns true if the given project's description is synchronized with
	 * the project description file on disk, and false otherwise.
	 */
	public boolean isDescriptionSynchronized(IProject target) {
		//sync info is stored on the description file, and on project info.
		//when the file is changed by someone else, the project info modification
		//stamp will be out of date
		IFile descriptionFile = target.getFile(IProjectDescription.DESCRIPTION_FILE_NAME);
		ResourceInfo projectInfo = ((Resource) target).getResourceInfo(false, false);
		if (projectInfo == null)
			return false;
		return projectInfo.getLocalSyncInfo() == getStore(descriptionFile).fetchInfo().getLastModified();
	}

	/**
	 * Returns true if the given resource is synchronized with the file system
	 * to the given depth.  Returns false otherwise.
	 *
	 * Any discovered out-of-sync resources are scheduled to be brought 
	 * back in sync, if {@link ResourcesPlugin#PREF_LIGHTWEIGHT_AUTO_REFRESH} is
	 * enabled.
	 *
	 * @see IResource#isSynchronized(int)
	 */
	public boolean isSynchronized(IResource target, int depth) {
		switch (target.getType()) {
			case IResource.ROOT :
				if (depth == IResource.DEPTH_ZERO)
					return true;
				//check sync on child projects.
				depth = depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : depth;
				IProject[] projects = ((IWorkspaceRoot) target).getProjects(IContainer.INCLUDE_HIDDEN);
				for (int i = 0; i < projects.length; i++) {
					if (!isSynchronized(projects[i], depth))
						return false;
				}
				return true;
			case IResource.PROJECT :
				if (!target.isAccessible())
					return true;
				break;
			case IResource.FOLDER :
				if (fastIsSynchronized((Folder) target))
					return true;
				break;
			case IResource.FILE :
				if (fastIsSynchronized((File) target))
					return true;
				break;
		}
		IsSynchronizedVisitor visitor = new IsSynchronizedVisitor(Policy.monitorFor(null));
		UnifiedTree tree = new UnifiedTree(target);
		try {
			tree.accept(visitor, depth);
		} catch (CoreException e) {
			Policy.log(e);
			return false;
		} catch (IsSynchronizedVisitor.ResourceChangedException e) {
			// Ask refresh manager to bring out-of-sync resource back into sync when convenient
			asyncRefresh(e.target);
			//visitor throws an exception if out of sync
			return false;
		}
		return true;
	}

	/**
	 * Check whether the preference {@link ResourcesPlugin#PREF_LIGHTWEIGHT_AUTO_REFRESH} is
	 * enabled.  When this preference is true the Resources plugin automatically refreshes
	 * resources which are known to be out-of-sync, and may install lightweight filesystem
	 * notification hooks.
	 * @return whether this FSRM is automatically refreshing discovered out-of-sync resources
	 */
	public boolean isLightweightAutoRefreshEnabled() {
		return lightweightAutoRefreshEnabled;
	}

	public void link(Resource target, URI location, IFileInfo fileInfo) throws CoreException {
		initializeStore(target, location);
		ResourceInfo info = target.getResourceInfo(false, true);
		long lastModified = fileInfo == null ? 0 : fileInfo.getLastModified();
		if (lastModified == 0)
			info.clearModificationStamp();
		updateLocalSync(info, lastModified);
	}

	/**
	 * Returns the resolved, absolute file system location of the given resource.
	 * Returns null if the location could not be resolved.
	 */
	public IPath locationFor(IResource target) {
		return getStoreRoot(target).localLocation(target.getFullPath(), target);
	}

	/**
	 * Returns the resolved, absolute file system location of the given resource.
	 * Returns null if the location could not be resolved.
	 */
	public URI locationURIFor(IResource target) {
		return getStoreRoot(target).computeURI(target.getFullPath());
	}

	public void move(IResource source, IFileStore destination, int flags, IProgressMonitor monitor) throws CoreException {
		//TODO figure out correct semantics for case where destination exists on disk
		getStore(source).move(destination, EFS.NONE, monitor);
	}

	public void propertyChange(PropertyChangeEvent event) {
		if (ResourcesPlugin.PREF_LIGHTWEIGHT_AUTO_REFRESH.equals(event.getProperty()))
			lightweightAutoRefreshEnabled = Boolean.valueOf(event.getNewValue().toString());
	}

	public InputStream read(IFile target, boolean force, IProgressMonitor monitor) throws CoreException {
		IFileStore store = getStore(target);
		if (lightweightAutoRefreshEnabled || !force) {
			final IFileInfo fileInfo = store.fetchInfo();
			if (!fileInfo.exists()) {
				asyncRefresh(target);
				if (!force) {
					String message = NLS.bind(Messages.localstore_fileNotFound, store.toString());
					throw new ResourceException(IResourceStatus.FAILED_READ_LOCAL, target.getFullPath(), message, null);
				}
			}
			ResourceInfo info = ((Resource) target).getResourceInfo(true, false);
			int flags = ((Resource) target).getFlags(info);
			((Resource) target).checkExists(flags, true);
			if (fileInfo.getLastModified() != info.getLocalSyncInfo()) {
				asyncRefresh(target);
				if (!force) {
					String message = NLS.bind(Messages.localstore_resourceIsOutOfSync, target.getFullPath());
					throw new ResourceException(IResourceStatus.OUT_OF_SYNC_LOCAL, target.getFullPath(), message, null);
				}
			}
		}
		return store.openInputStream(EFS.NONE, monitor);
	}

	/**
	 * Reads and returns the project description for the given project.
	 * Never returns null.
	 * @param target the project whose description should be read.
	 * @param creation true if this project is just being created, in which
	 * case the private project information (including the location) needs to be read 
	 * from disk as well.
	 * @exception CoreException if there was any failure to read the project
	 * description, or if the description was missing.
	 */
	public ProjectDescription read(IProject target, boolean creation) throws CoreException {
		IProgressMonitor monitor = Policy.monitorFor(null);
		
		//read the project location if this project is being created
		URI projectLocation = null;
		ProjectDescription privateDescription = null;
		if (creation) {
			privateDescription = new ProjectDescription();
			getWorkspace().getMetaArea().readPrivateDescription(target, privateDescription);
			projectLocation = privateDescription.getLocationURI();
		} else {
			IProjectDescription description = ((Project) target).internalGetDescription();
			if (description != null && description.getLocationURI() != null) {
				projectLocation = description.getLocationURI();
			}
		}
		final boolean isDefaultLocation = projectLocation == null;
		if (isDefaultLocation) {
			projectLocation = URIUtil.toURI(getProjectDefaultLocation(target));
		}
		IFileStore projectStore = initializeStore(target, projectLocation);
		IFileStore descriptionStore = projectStore.getChild(IProjectDescription.DESCRIPTION_FILE_NAME);
		ProjectDescription description = null;
		//hold onto any exceptions until after sync info is updated, then throw it
		ResourceException error = null;
		InputStream in = null;
		try {
			in = new BufferedInputStream(descriptionStore.openInputStream(EFS.NONE, monitor));
			// IFileStore#openInputStream may cancel the monitor, thus the monitor state is checked
			Policy.checkCanceled(monitor);
			description = new ProjectDescriptionReader(target).read(new InputSource(in));
		} catch (OperationCanceledException e) {
			String msg = NLS.bind(Messages.resources_missingProjectMeta, target.getName());
			throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, e);
		} catch (CoreException e) {
			//try the legacy location in the meta area
			description = getWorkspace().getMetaArea().readOldDescription(target);
			if (description != null)
				return description;
			if (!descriptionStore.fetchInfo().exists()) {
				String msg = NLS.bind(Messages.resources_missingProjectMeta, target.getName());
				throw new ResourceException(IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, null);
			}
			String msg = NLS.bind(Messages.resources_readProjectMeta, target.getName());
			error = new ResourceException(IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, e);
		} finally {
			FileUtil.safeClose(in);
		}
		if (error == null && description == null) {
			String msg = NLS.bind(Messages.resources_readProjectMeta, target.getName());
			error = new ResourceException(IResourceStatus.FAILED_READ_METADATA, target.getFullPath(), msg, null);
		}
		if (description != null) {
			if (!isDefaultLocation)
				description.setLocationURI(projectLocation);
			if (creation && privateDescription != null)
				// Bring dynamic state back to life
				description.updateDynamicState(privateDescription);
		}
		long lastModified = descriptionStore.fetchInfo().getLastModified();
		IFile descriptionFile = target.getFile(IProjectDescription.DESCRIPTION_FILE_NAME);
		//don't get a mutable copy because we might be in restore which isn't an operation
		//it doesn't matter anyway because local sync info is not included in deltas
		ResourceInfo info = ((Resource) descriptionFile).getResourceInfo(false, false);
		if (info == null) {
			//create a new resource on the sly -- don't want to start an operation
			info = getWorkspace().createResource(descriptionFile, false);
			updateLocalSync(info, lastModified);
		}
		//if the project description has changed between sessions, let it remain
		//out of sync -- that way link changes will be reconciled on next refresh
		if (!creation)
			updateLocalSync(info, lastModified);

		//update the timestamp on the project as well so we know when it has
		//been changed from the outside
		info = ((Resource) target).getResourceInfo(false, true);
		updateLocalSync(info, lastModified);

		if (error != null)
			throw error;
		return description;
	}

	public boolean refresh(IResource target, int depth, boolean updateAliases, IProgressMonitor monitor) throws CoreException {
		switch (target.getType()) {
			case IResource.ROOT :
				return refreshRoot((IWorkspaceRoot) target, depth, updateAliases, monitor);
			case IResource.PROJECT :
				if (!target.isAccessible())
					return false;
				//fall through
			case IResource.FOLDER :
			case IResource.FILE :
				return refreshResource(target, depth, updateAliases, monitor);
		}
		return false;
	}

	protected boolean refreshResource(IResource target, int depth, boolean updateAliases, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		int totalWork = RefreshLocalVisitor.TOTAL_WORK;
		String title = NLS.bind(Messages.localstore_refreshing, target.getFullPath());
		try {
			monitor.beginTask(title, totalWork);
			RefreshLocalVisitor visitor = updateAliases ? new RefreshLocalAliasVisitor(monitor) : new RefreshLocalVisitor(monitor);
			IFileStore fileStore = ((Resource) target).getStore();
			//try to get all info in one shot, if file system supports it
			IFileTree fileTree = fileStore.getFileSystem().fetchFileTree(fileStore, new SubProgressMonitor(monitor, 0));
			UnifiedTree tree = fileTree == null ? new UnifiedTree(target) : new UnifiedTree(target, fileTree);
			tree.accept(visitor, depth);
			IStatus result = visitor.getErrorStatus();
			if (!result.isOK())
				throw new ResourceException(result);
			return visitor.resourcesChanged();
		} finally {
			monitor.done();
		}
	}

	/**
	 * Synchronizes the entire workspace with the local file system.
	 * The current implementation does this by synchronizing each of the
	 * projects currently in the workspace.  A better implementation may
	 * be possible.
	 */
	protected boolean refreshRoot(IWorkspaceRoot target, int depth, boolean updateAliases, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		IProject[] projects = target.getProjects(IContainer.INCLUDE_HIDDEN);
		int totalWork = projects.length;
		String title = Messages.localstore_refreshingRoot;
		try {
			monitor.beginTask(title, totalWork);
			// if doing depth zero, there is nothing to do (can't refresh the root).  
			// Note that we still need to do the beginTask, done pair.
			if (depth == IResource.DEPTH_ZERO)
				return false;
			boolean changed = false;
			// drop the depth by one level since processing the root counts as one level.
			depth = depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : depth;
			for (int i = 0; i < projects.length; i++)
				changed |= refresh(projects[i], depth, updateAliases, Policy.subMonitorFor(monitor, 1));
			return changed;
		} finally {
			monitor.done();
		}
	}

	/**
	 * Returns the resource corresponding to the given workspace path.  The
	 * "files" parameter is used for paths of two or more segments.  If true,
	 * a file is returned, otherwise a folder is returned.  Returns null if files is true
	 * and the path is not of sufficient length.
	 */
	protected IResource resourceFor(IPath path, boolean files) {
		int numSegments = path.segmentCount();
		if (files && numSegments < ICoreConstants.MINIMUM_FILE_SEGMENT_LENGTH)
			return null;
		IWorkspaceRoot root = getWorkspace().getRoot();
		if (path.isRoot())
			return root;
		if (numSegments == 1)
			return root.getProject(path.segment(0));
		return files ? (IResource) root.getFile(path) : (IResource) root.getFolder(path);
	}

	/* (non-javadoc)
	 * @see IResouce.setLocalTimeStamp
	 */
	public long setLocalTimeStamp(IResource target, ResourceInfo info, long value) throws CoreException {
		IFileStore store = getStore(target);
		IFileInfo fileInfo = store.fetchInfo();
		fileInfo.setLastModified(value);
		store.putInfo(fileInfo, EFS.SET_LAST_MODIFIED, null);
		//actual value may be different depending on file system granularity
		fileInfo = store.fetchInfo();
		long actualValue = fileInfo.getLastModified();
		updateLocalSync(info, actualValue);
		return actualValue;
	}

	/**
	 * The storage location for a resource has changed; update the location.
	 * @param target
	 * @param info
	 * @param location
	 */
	public void setLocation(IResource target, ResourceInfo info, URI location) {
		FileStoreRoot oldRoot = info.getFileStoreRoot();
		if (location != null) {
			info.setFileStoreRoot(new FileStoreRoot(location, target.getFullPath()));
		} else {
			//project is in default location so clear the store root
			info.setFileStoreRoot(null);
		}
		if (oldRoot != null)
			oldRoot.setValid(false);
	}

	/* (non-javadoc)
	 * @see IResource.setResourceAttributes
	 */
	public void setResourceAttributes(IResource resource, ResourceAttributes attributes) throws CoreException {
		IFileStore store = getStore(resource);
		//when the executable bit is changed on a folder a refresh is required
		boolean refresh = false;
		if (resource instanceof IContainer && ((store.getFileSystem().attributes() & EFS.ATTRIBUTE_EXECUTABLE) != 0))
			refresh = store.fetchInfo().getAttribute(EFS.ATTRIBUTE_EXECUTABLE) != attributes.isExecutable();
		store.putInfo(FileUtil.attributesToFileInfo(attributes), EFS.SET_ATTRIBUTES, null);
		//must refresh in the background because we are not inside an operation
		if (refresh)
			workspace.getRefreshManager().refresh(resource);
	}

	public void shutdown(IProgressMonitor monitor) throws CoreException {
		if (_historyStore != null)
			_historyStore.shutdown(monitor);
		ResourcesPlugin.getPlugin().getPluginPreferences().removePropertyChangeListener(this);
	}

	public void startup(IProgressMonitor monitor) throws CoreException {
		Preferences preferences = ResourcesPlugin.getPlugin().getPluginPreferences();
		preferences.addPropertyChangeListener(this);
		lightweightAutoRefreshEnabled = preferences.getBoolean(ResourcesPlugin.PREF_LIGHTWEIGHT_AUTO_REFRESH);
	}

	/**
	 * The ResourceInfo must be mutable.
	 */
	public void updateLocalSync(ResourceInfo info, long localSyncInfo) {
		info.setLocalSyncInfo(localSyncInfo);
		if (localSyncInfo == I_NULL_SYNC_INFO)
			info.clear(M_LOCAL_EXISTS);
		else
			info.set(M_LOCAL_EXISTS);
	}

	/**
	 * The target must exist in the workspace. The content InputStream is
	 * closed even if the method fails. If the force flag is false we only write
	 * the file if it does not exist or if it is already local and the timestamp
	 * has NOT changed since last synchronization, otherwise a CoreException
	 * is thrown.
	 */
	public void write(IFile target, InputStream content, IFileInfo fileInfo, int updateFlags, boolean append, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(null);
		try {
			IFileStore store = getStore(target);
			if (fileInfo.getAttribute(EFS.ATTRIBUTE_READ_ONLY)) {
				String message = NLS.bind(Messages.localstore_couldNotWriteReadOnly, target.getFullPath());
				throw new ResourceException(IResourceStatus.FAILED_WRITE_LOCAL, target.getFullPath(), message, null);
			}
			long lastModified = fileInfo.getLastModified();
			if (BitMask.isSet(updateFlags, IResource.FORCE)) {
				if (append && !target.isLocal(IResource.DEPTH_ZERO) && !fileInfo.exists()) {
					// force=true, local=false, existsInFileSystem=false
					String message = NLS.bind(Messages.resources_mustBeLocal, target.getFullPath());
					throw new ResourceException(IResourceStatus.RESOURCE_NOT_LOCAL, target.getFullPath(), message, null);
				}
			} else {
				if (target.isLocal(IResource.DEPTH_ZERO)) {
					ResourceInfo info = ((Resource) target).getResourceInfo(true, false);
					// test if timestamp is the same since last synchronization
					if (lastModified != info.getLocalSyncInfo()) {
						asyncRefresh(target);
						String message = NLS.bind(Messages.localstore_resourceIsOutOfSync, target.getFullPath());
						throw new ResourceException(IResourceStatus.OUT_OF_SYNC_LOCAL, target.getFullPath(), message, null);
					}
					if (!fileInfo.exists()) {
						asyncRefresh(target);
						String message = NLS.bind(Messages.localstore_resourceDoesNotExist, target.getFullPath());
						throw new ResourceException(IResourceStatus.NOT_FOUND_LOCAL, target.getFullPath(), message, null);
					}
				} else {
					if (fileInfo.exists()) {
						String message = NLS.bind(Messages.localstore_resourceExists, target.getFullPath());
						throw new ResourceException(IResourceStatus.EXISTS_LOCAL, target.getFullPath(), message, null);
					}
					if (append) {
						String message = NLS.bind(Messages.resources_mustBeLocal, target.getFullPath());
						throw new ResourceException(IResourceStatus.RESOURCE_NOT_LOCAL, target.getFullPath(), message, null);
					}
				}
			}
			// add entry to History Store.
			if (BitMask.isSet(updateFlags, IResource.KEEP_HISTORY) && fileInfo.exists())
				//never move to the history store, because then the file is missing if write fails
				getHistoryStore().addState(target.getFullPath(), store, fileInfo, false);
			if (!fileInfo.exists())
				store.getParent().mkdir(EFS.NONE, null);
			int options = append ? EFS.APPEND : EFS.NONE;
			OutputStream out = store.openOutputStream(options, Policy.subMonitorFor(monitor, 0));
			FileUtil.transferStreams(content, out, store.toString(), monitor);
			// get the new last modified time and stash in the info
			lastModified = store.fetchInfo().getLastModified();
			ResourceInfo info = ((Resource) target).getResourceInfo(false, true);
			updateLocalSync(info, lastModified);
			info.incrementContentId();
			info.clear(M_CONTENT_CACHE);
			workspace.updateModificationStamp(info);
		} finally {
			FileUtil.safeClose(content);
		}
	}

	/**
	 * If force is false, this method fails if there is already a resource in
	 * target's location.
	 */
	public void write(IFolder target, boolean force, IProgressMonitor monitor) throws CoreException {
		IFileStore store = getStore(target);
		if (!force) {
			IFileInfo fileInfo = store.fetchInfo();
			if (fileInfo.isDirectory()) {
				String message = NLS.bind(Messages.localstore_resourceExists, target.getFullPath());
				throw new ResourceException(IResourceStatus.EXISTS_LOCAL, target.getFullPath(), message, null);
			}
			if (fileInfo.exists()) {
				String message = NLS.bind(Messages.localstore_fileExists, target.getFullPath());
				throw new ResourceException(IResourceStatus.OUT_OF_SYNC_LOCAL, target.getFullPath(), message, null);
			}
		}
		store.mkdir(EFS.NONE, monitor);
		ResourceInfo info = ((Resource) target).getResourceInfo(false, true);
		updateLocalSync(info, store.fetchInfo().getLastModified());
	}

	/**
	 * Write the .project file without modifying the resource tree.  This is called
	 * during save when it is discovered that the .project file is missing.  The tree
	 * cannot be modified during save.
	 */
	public void writeSilently(IProject target) throws CoreException {
		IPath location = locationFor(target);
		//if the project location cannot be resolved, we don't know if a description file exists or not
		if (location == null)
			return;
		IFileStore projectStore = getStore(target);
		projectStore.mkdir(EFS.NONE, null);
		//can't do anything if there's no description
		IProjectDescription desc = ((Project) target).internalGetDescription();
		if (desc == null)
			return;
		//write the project's private description to the meta-data area
		getWorkspace().getMetaArea().writePrivateDescription(target);

		//write the file that represents the project description
		IFileStore fileStore = projectStore.getChild(IProjectDescription.DESCRIPTION_FILE_NAME);
		OutputStream out = null;
		try {
			out = fileStore.openOutputStream(EFS.NONE, null);
			new ModelObjectWriter().write(desc, out);
			out.close();
		} catch (IOException e) {
			String msg = NLS.bind(Messages.resources_writeMeta, target.getFullPath());
			throw new ResourceException(IResourceStatus.FAILED_WRITE_METADATA, target.getFullPath(), msg, e);
		} finally {
			FileUtil.safeClose(out);
		}
		//for backwards compatibility, ensure the old .prj file is deleted
		getWorkspace().getMetaArea().clearOldDescription(target);
	}
}
