platform35/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ContentDescriptionManager.java
changeset 40 eb3c938c7fef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/platform35/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ContentDescriptionManager.java	Thu Jul 30 11:56:23 2009 -0500
@@ -0,0 +1,520 @@
+/*******************************************************************************
+ * Copyright (c) 2004, 2009 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
+ *******************************************************************************/
+package org.eclipse.core.internal.resources;
+
+import org.eclipse.core.internal.utils.FileUtil;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.internal.events.ILifecycleListener;
+import org.eclipse.core.internal.events.LifecycleEvent;
+import org.eclipse.core.internal.utils.*;
+import org.eclipse.core.internal.watson.*;
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.content.*;
+import org.eclipse.core.runtime.content.IContentTypeManager.ContentTypeChangeEvent;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.osgi.util.NLS;
+import org.osgi.framework.Bundle;
+
+/**
+ * Keeps a cache of recently read content descriptions.
+ * 
+ * @since 3.0
+ * @see IFile#getContentDescription()
+ */
+public class ContentDescriptionManager implements IManager, IRegistryChangeListener, IContentTypeManager.IContentTypeChangeListener, ILifecycleListener {
+	/**
+	 * This job causes the content description cache and the related flags 
+	 * in the resource tree to be flushed. 
+	 */
+	private class FlushJob extends WorkspaceJob {
+		private final List toFlush;
+		private boolean fullFlush;
+
+		public FlushJob() {
+			super(Messages.resources_flushingContentDescriptionCache);
+			setSystem(true);
+			setUser(false);
+			setPriority(LONG);
+			setRule(workspace.getRoot());
+			toFlush = new ArrayList(5);
+		}
+
+		/* (non-Javadoc)
+		 * See Job#belongsTo(Object)
+		 */
+		public boolean belongsTo(Object family) {
+			return FAMILY_DESCRIPTION_CACHE_FLUSH.equals(family);
+		}
+
+		/* (non-Javadoc)
+		 * See WorkspaceJob#runInWorkspace(IProgressMonitor)
+		 */
+		public IStatus runInWorkspace(final IProgressMonitor monitor) {
+			if (monitor.isCanceled())
+				return Status.CANCEL_STATUS;
+			try {
+				monitor.beginTask("", Policy.opWork); //$NON-NLS-1$
+				//note that even though we are running in a workspace job, we
+				//must do a begin/endOperation to re-acquire the workspace lock
+				final ISchedulingRule rule = workspace.getRoot();
+				try {
+					workspace.prepareOperation(rule, monitor);
+					workspace.beginOperation(true);
+					//don't do anything if the system is shutting down or has been shut down
+					//it is too late to change the workspace at this point anyway
+					if (systemBundle.getState() != Bundle.STOPPING)
+						doFlushCache(monitor, getPathsToFlush());
+				} finally {
+					workspace.endOperation(rule, false, Policy.subMonitorFor(monitor, Policy.endOpWork));
+				}
+			} catch (OperationCanceledException e) {
+				return Status.CANCEL_STATUS;
+			} catch (CoreException e) {
+				return e.getStatus();
+			} finally {
+				monitor.done();
+			}
+			return Status.OK_STATUS;
+		}
+
+		private IPath[] getPathsToFlush() {
+			synchronized (toFlush) {
+				try {
+					if (fullFlush)
+						return null;
+					int size = toFlush.size();
+					return (size == 0) ? null : (IPath[]) toFlush.toArray(new IPath[size]);
+				} finally {
+					fullFlush = false;
+					toFlush.clear();
+				}
+			}
+		}
+
+		/**
+		 * @param project project to flush, or null for a full flush
+		 */
+		void flush(IProject project) {
+			if (Policy.DEBUG_CONTENT_TYPE_CACHE)
+				Policy.debug("Scheduling flushing of content type cache for " + (project == null ? Path.ROOT : project.getFullPath())); //$NON-NLS-1$
+			synchronized (toFlush) {
+				if (!fullFlush)
+					if (project == null)
+						fullFlush = true;
+					else
+						toFlush.add(project.getFullPath());
+			}
+			schedule(1000);
+		}
+
+	}
+
+	/** 
+	 * An input stream that only opens the file if bytes are actually requested.
+	 * @see #readDescription(File)
+	 */
+	class LazyFileInputStream extends InputStream {
+		private InputStream actual;
+		private IFileStore target;
+
+		LazyFileInputStream(IFileStore target) {
+			this.target = target;
+		}
+
+		public int available() throws IOException {
+			if (actual == null)
+				return 0;
+			return actual.available();
+		}
+
+		public void close() throws IOException {
+			if (actual == null)
+				return;
+			actual.close();
+		}
+
+		private void ensureOpened() throws IOException {
+			if (actual != null)
+				return;
+			if (target == null)
+				throw new FileNotFoundException();
+			try {
+				actual = target.openInputStream(EFS.NONE, null);
+			} catch (CoreException e) {
+				throw new IOException(e.getMessage());
+			}
+		}
+
+		public int read() throws IOException {
+			ensureOpened();
+			return actual.read();
+		}
+
+		public int read(byte[] b, int off, int len) throws IOException {
+			ensureOpened();
+			return actual.read(b, off, len);
+		}
+
+		public long skip(long n) throws IOException {
+			ensureOpened();
+			return actual.skip(n);
+		}
+	}
+
+	private static final QualifiedName CACHE_STATE = new QualifiedName(ResourcesPlugin.PI_RESOURCES, "contentCacheState"); //$NON-NLS-1$
+	private static final QualifiedName CACHE_TIMESTAMP = new QualifiedName(ResourcesPlugin.PI_RESOURCES, "contentCacheTimestamp"); //$NON-NLS-1$\
+
+	public static final String FAMILY_DESCRIPTION_CACHE_FLUSH = ResourcesPlugin.PI_RESOURCES + ".contentDescriptionCacheFamily"; //$NON-NLS-1$	
+
+	//possible values for the CACHE_STATE property
+	public static final byte EMPTY_CACHE = 1;
+	public static final byte USED_CACHE = 2;
+	public static final byte INVALID_CACHE = 3;
+	public static final byte FLUSHING_CACHE = 4;
+	
+	// This state indicates that FlushJob is scheduled and full flush is going to be performed.
+	// In the meantime the cache was discarded. It is used as a temporary cache till the FlushJob start.
+	public static final byte ABOUT_TO_FLUSH = 5;
+
+	private static final String PT_CONTENTTYPES = "contentTypes"; //$NON-NLS-1$
+
+	private Cache cache;
+
+	private byte cacheState;
+
+	private FlushJob flushJob;
+	private ProjectContentTypes projectContentTypes;
+
+	Workspace workspace;
+	protected final Bundle systemBundle = Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$
+
+	/**
+	 * @see IContentTypeManager.IContentTypeChangeListener#contentTypeChanged(IContentTypeManager.ContentTypeChangeEvent)
+	 */
+	public void contentTypeChanged(ContentTypeChangeEvent event) {
+		if (Policy.DEBUG_CONTENT_TYPE)
+			Policy.debug("Content type settings changed for " + event.getContentType()); //$NON-NLS-1$
+		invalidateCache(true, null);
+	}
+
+	synchronized void doFlushCache(final IProgressMonitor monitor, IPath[] toClean) throws CoreException {
+		// nothing to be done if no information cached
+		if (getCacheState() != INVALID_CACHE && getCacheState() != ABOUT_TO_FLUSH) {
+			if (Policy.DEBUG_CONTENT_TYPE_CACHE)
+				Policy.debug("Content type cache flush not performed"); //$NON-NLS-1$
+			return;
+		}
+		try {
+			setCacheState(FLUSHING_CACHE);
+			// flush the MRU cache
+			cache.discardAll();
+			if (toClean == null || toClean.length == 0)
+				// no project was added, must be a global flush
+				clearContentFlags(Path.ROOT, monitor);
+			else {
+				// flush a project at a time								
+				for (int i = 0; i < toClean.length; i++)
+					clearContentFlags(toClean[i], monitor);
+			}
+		} catch (CoreException ce) {
+			setCacheState(INVALID_CACHE);
+			throw ce;
+		}
+		// done cleaning (only if we didn't fail)
+		setCacheState(EMPTY_CACHE);
+	}
+
+	/**
+	 * Clears the content related flags for every file under the given root.
+	 */
+	private void clearContentFlags(IPath root, final IProgressMonitor monitor) {
+		long flushStart = System.currentTimeMillis();
+		if (Policy.DEBUG_CONTENT_TYPE_CACHE)
+			Policy.debug("Flushing content type cache for " + root); //$NON-NLS-1$		
+		// discard content type related flags for all files in the tree 
+		IElementContentVisitor visitor = new IElementContentVisitor() {
+			public boolean visitElement(ElementTree tree, IPathRequestor requestor, Object elementContents) {
+				if (monitor.isCanceled())
+					throw new OperationCanceledException();
+				if (elementContents == null)
+					return false;
+				ResourceInfo info = (ResourceInfo) elementContents;
+				if (info.getType() != IResource.FILE)
+					return true;
+				info = workspace.getResourceInfo(requestor.requestPath(), false, true);
+				if (info == null)
+					return false;
+				info.clear(ICoreConstants.M_CONTENT_CACHE);
+				return true;
+			}
+		};
+		new ElementTreeIterator(workspace.getElementTree(), root).iterate(visitor);
+		if (Policy.DEBUG_CONTENT_TYPE_CACHE)
+			Policy.debug("Content type cache for " + root + " flushed in " + (System.currentTimeMillis() - flushStart) + " ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+	}
+
+	Cache getCache() {
+		return cache;
+	}
+
+	/** Public so tests can examine it. */
+	public synchronized byte getCacheState() {
+		if (cacheState != 0)
+			// we have read/set it before, no nead to read property
+			return cacheState;
+		String persisted;
+		try {
+			persisted = workspace.getRoot().getPersistentProperty(CACHE_STATE);
+			cacheState = persisted != null ? Byte.parseByte(persisted) : INVALID_CACHE;
+		} catch (NumberFormatException e) {
+			cacheState = INVALID_CACHE;
+		} catch (CoreException e) {
+			Policy.log(e.getStatus());
+			cacheState = INVALID_CACHE;
+		}
+		return cacheState;
+	}
+
+	public long getCacheTimestamp() throws CoreException {
+		try {
+			return Long.parseLong(workspace.getRoot().getPersistentProperty(CACHE_TIMESTAMP));
+		} catch (NumberFormatException e) {
+			return 0;
+		}
+	}
+
+	public IContentTypeMatcher getContentTypeMatcher(Project project) throws CoreException {
+		return projectContentTypes.getMatcherFor(project);
+	}
+
+	public IContentDescription getDescriptionFor(File file, ResourceInfo info) throws CoreException {
+		if (ProjectContentTypes.usesContentTypePreferences(file.getFullPath().segment(0)))
+			// caching for project containing project specific settings is not supported
+			return readDescription(file);
+		if (getCacheState() == INVALID_CACHE) {
+			// discard the cache, so it can be used before the flush job starts
+			setCacheState(ABOUT_TO_FLUSH);
+			cache.discardAll();
+			// the cache is not good, flush it
+			flushJob.schedule(1000);
+		}
+		if (getCacheState() != ABOUT_TO_FLUSH) {
+			// first look for the flags in the resource info to avoid looking in the cache
+			// don't need to copy the info because the modified bits are not in the deltas
+			if (info == null)
+				return null;
+			if (info.isSet(ICoreConstants.M_NO_CONTENT_DESCRIPTION))
+				// presumably, this file has no known content type
+				return null;
+			if (info.isSet(ICoreConstants.M_DEFAULT_CONTENT_DESCRIPTION)) {
+				// this file supposedly has a default content description for an "obvious" content type			
+				IContentTypeManager contentTypeManager = Platform.getContentTypeManager();
+				// try to find the obvious content type matching its name
+				IContentType type = contentTypeManager.findContentTypeFor(file.getName());
+				if (type != null)
+					// we found it, we are done
+					return type.getDefaultDescription();
+				// for some reason, there was no content type for this file name
+				// fix this and keep going			
+				info.clear(ICoreConstants.M_CONTENT_CACHE);
+			}
+		}
+		synchronized (this) {
+			// tries to get a description from the cache	
+			Cache.Entry entry = cache.getEntry(file.getFullPath());
+			if (entry != null && entry.getTimestamp() == getTimestamp(info))
+				// there was a description in the cache, and it was up to date
+				return (IContentDescription) entry.getCached();
+		}
+		
+		// either we didn't find a description in the cache, or it was not up-to-date - has to be read again
+		// reading description can call 3rd party code, so don't synchronize it
+		IContentDescription newDescription = readDescription(file);
+		
+		synchronized (this) {
+			// tries to get a description from the cache
+			Cache.Entry entry = cache.getEntry(file.getFullPath());
+			if (entry != null && entry.getTimestamp() == getTimestamp(info))
+				// there was a description in the cache, and it was up to date
+				return (IContentDescription) entry.getCached();
+			
+			if (getCacheState() != ABOUT_TO_FLUSH) {
+				// we are going to add an entry to the cache or update the resource info - remember that
+				setCacheState(USED_CACHE);
+				if (newDescription == null) {
+					// no content type exists for this file name/contents - remember this
+					info.set(ICoreConstants.M_NO_CONTENT_DESCRIPTION);
+					return null;
+				}
+				if (newDescription.getContentType().getDefaultDescription().equals(newDescription)) {
+					// we got a default description
+					IContentType defaultForName = Platform.getContentTypeManager().findContentTypeFor(file.getName());
+					if (newDescription.getContentType().equals(defaultForName)) {
+						// it is a default description for the obvious content type given its file name, we don't have to cache					
+						info.set(ICoreConstants.M_DEFAULT_CONTENT_DESCRIPTION);
+						return newDescription;
+					}
+				}
+			}
+			// we actually got a description filled by a describer (or a default description for a non-obvious type)
+			if (entry == null)
+				// there was no entry before - create one
+				entry = cache.addEntry(file.getFullPath(), newDescription, getTimestamp(info));
+			else {
+				// just update the existing entry
+				entry.setTimestamp(info.getContentId());
+				entry.setCached(newDescription);
+			}
+			return newDescription;
+		}
+	}
+
+	/**
+	 * Returns a timestamp that uniquely identifies a particular content state
+	 * of a particular resource. For use as a key in a content type cache.
+	 */
+	private long getTimestamp(ResourceInfo info) {
+		return info.getContentId() + info.getNodeId();
+	}
+
+	/**
+	 * Marks the cache as invalid. Does not do anything if the cache is new.
+	 * Optionally causes the cached information to be actually flushed.
+	 *  
+	 * @param flush whether the cached information should be flushed 
+	 * @see #doFlushCache(IProgressMonitor, IPath[])
+	 */
+	public synchronized void invalidateCache(boolean flush, IProject project) {
+		if (getCacheState() == EMPTY_CACHE)
+			// cache has not been touched, nothing to do			
+			return;
+		// mark the cache as invalid
+		try {
+			setCacheState(INVALID_CACHE);
+		} catch (CoreException e) {
+			Policy.log(e.getStatus());
+		}
+		if (Policy.DEBUG_CONTENT_TYPE_CACHE)
+			Policy.debug("Invalidated cache for " + (project == null ? Path.ROOT : project.getFullPath())); //$NON-NLS-1$		
+		if (flush) {
+			try {
+				// discard the cache, so it can be used before the flush job starts
+				setCacheState(ABOUT_TO_FLUSH);
+				cache.discardAll();
+			} catch (CoreException e) {
+				Policy.log(e.getStatus());
+			}
+			// the cache is not good, flush it
+			flushJob.flush(project);
+		}
+	}
+
+	/**
+	 * Tries to obtain a content description for the given file.  
+	 */
+	private IContentDescription readDescription(File file) throws CoreException {
+		if (Policy.DEBUG_CONTENT_TYPE)
+			Policy.debug("reading contents of " + file); //$NON-NLS-1$		
+		// tries to obtain a description for this file contents
+		InputStream contents = new LazyFileInputStream(file.getStore());
+		try {
+			IContentTypeMatcher matcher = getContentTypeMatcher((Project) file.getProject());
+			return matcher.getDescriptionFor(contents, file.getName(), IContentDescription.ALL);
+		} catch (IOException e) {
+			String message = NLS.bind(Messages.resources_errorContentDescription, file.getFullPath());
+			throw new ResourceException(IResourceStatus.FAILED_DESCRIBING_CONTENTS, file.getFullPath(), message, e);
+		} finally {
+			FileUtil.safeClose(contents);
+		}
+	}
+
+	/**
+	 * @see IRegistryChangeListener#registryChanged(IRegistryChangeEvent)
+	 */
+	public void registryChanged(IRegistryChangeEvent event) {
+		// no changes related to the content type registry
+		if (event.getExtensionDeltas(Platform.PI_RUNTIME, PT_CONTENTTYPES).length == 0)
+			return;
+		invalidateCache(true, null);
+	}
+
+	/**
+	 * @see ILifecycleListener#handleEvent(LifecycleEvent)
+	 */
+	public void handleEvent(LifecycleEvent event) {
+		//TODO are these the only events we care about?
+		switch (event.kind) {
+			case LifecycleEvent.PRE_PROJECT_CHANGE :
+			// if the project changes, its natures may have changed as well (content types may be associated to natures)
+			case LifecycleEvent.PRE_PROJECT_DELETE :
+			// if the project gets deleted, we may get confused if it is recreated again (content ids might match)
+			case LifecycleEvent.PRE_PROJECT_MOVE :
+				// if the project moves, resource paths (used as keys in the in-memory cache) will have changed 
+				invalidateCache(true, (IProject) event.resource);
+		}
+	}
+
+	synchronized void setCacheState(byte newCacheState) throws CoreException {
+		if (cacheState == newCacheState)
+			return;
+		workspace.getRoot().setPersistentProperty(CACHE_STATE, Byte.toString(newCacheState));
+		cacheState = newCacheState;
+	}
+
+	private void setCacheTimeStamp(long timeStamp) throws CoreException {
+		workspace.getRoot().setPersistentProperty(CACHE_TIMESTAMP, Long.toString(timeStamp));
+	}
+
+	public void shutdown(IProgressMonitor monitor) throws CoreException {
+		if (getCacheState() != INVALID_CACHE)
+			// remember the platform timestamp for which we have a valid cache 
+			setCacheTimeStamp(Platform.getStateStamp());
+		Platform.getContentTypeManager().removeContentTypeChangeListener(this);
+		Platform.getExtensionRegistry().removeRegistryChangeListener(this);
+		cache.dispose();
+		cache = null;
+		flushJob.cancel();
+		flushJob = null;
+		projectContentTypes = null;
+	}
+
+	public void startup(IProgressMonitor monitor) throws CoreException {
+		workspace = (Workspace) ResourcesPlugin.getWorkspace();
+		cache = new Cache(100, 1000, 0.1);
+		projectContentTypes = new ProjectContentTypes(workspace);
+		getCacheState();
+		if (cacheState == FLUSHING_CACHE || cacheState == ABOUT_TO_FLUSH)
+			// in case we died before completing the last flushing 
+			setCacheState(INVALID_CACHE);
+		flushJob = new FlushJob();
+		// the cache is stale (plug-ins that might be contributing content types were added/removed)
+		if (getCacheTimestamp() != Platform.getStateStamp())
+			invalidateCache(false, null);
+		// register a lifecycle listener
+		workspace.addLifecycleListener(this);
+		// register a content type change listener
+		Platform.getContentTypeManager().addContentTypeChangeListener(this);
+		// register a registry change listener		
+		Platform.getExtensionRegistry().addRegistryChangeListener(this, Platform.PI_RUNTIME);
+	}
+
+	public void projectPreferencesChanged(IProject project) {
+		if (Policy.DEBUG_CONTENT_TYPE)
+			Policy.debug("Project preferences changed for " + project); //$NON-NLS-1$		
+		projectContentTypes.contentTypePreferencesChanged(project);
+	}
+}