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