Index: src/org/eclipse/core/internal/resources/ContentDescriptionManager.java =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ContentDescriptionManager.java,v retrieving revision 1.19 diff -u -r1.19 ContentDescriptionManager.java --- src/org/eclipse/core/internal/resources/ContentDescriptionManager.java 21 Feb 2005 23:10:23 -0000 1.19 +++ src/org/eclipse/core/internal/resources/ContentDescriptionManager.java 14 Apr 2005 15:34:17 -0000 @@ -11,6 +11,8 @@ package org.eclipse.core.internal.resources; import java.io.*; +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.*; @@ -18,7 +20,6 @@ import org.eclipse.core.runtime.content.*; import org.eclipse.core.runtime.content.IContentTypeManager.ContentTypeChangeEvent; import org.eclipse.core.runtime.jobs.ISchedulingRule; -import org.eclipse.core.runtime.jobs.Job; import org.eclipse.osgi.util.NLS; /** @@ -27,13 +28,14 @@ * @since 3.0 * @see IFile#getContentDescription() */ -public class ContentDescriptionManager implements IManager, IRegistryChangeListener, IContentTypeManager.IContentTypeChangeListener { - +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 Queue toFlush; + private boolean fullFlush; public FlushJob() { super(Messages.resources_flushingContentDescriptionCache); @@ -41,6 +43,7 @@ setUser(false); setPriority(LONG); setRule(workspace.getRoot()); + toFlush = new Queue(); } /* (non-Javadoc) @@ -64,7 +67,7 @@ try { workspace.prepareOperation(rule, monitor); workspace.beginOperation(true); - doFlushCache(monitor); + doFlushCache(monitor, getPathsToFlush()); } finally { workspace.endOperation(rule, false, Policy.subMonitorFor(monitor, Policy.endOpWork)); } @@ -77,6 +80,31 @@ } return Status.OK_STATUS; } + + private IPath[] getPathsToFlush() { + synchronized (toFlush) { + if (fullFlush) + return null; + int size = toFlush.size(); + return (size == 0) ? null : (IPath[]) toFlush.copyTo(new IPath[size]); + } + } + + /** + * @param project project to flush, or null for a full flush + */ + void flush(IProject project) { + synchronized (toFlush) { + if (!fullFlush) + if (project != null) { + fullFlush = true; + toFlush.clear(); + } else + toFlush.add(project.getFullPath()); + } + schedule(1000); + } + } /** @@ -144,7 +172,8 @@ private byte cacheState; - private Job flushJob; + private FlushJob flushJob; + private ProjectContentTypes projectContentTypes; Workspace workspace; @@ -152,16 +181,44 @@ * @see IContentTypeManager.IContentTypeChangeListener#contentTypeChanged(ContentTypeChangeEvent) */ public void contentTypeChanged(ContentTypeChangeEvent event) { - invalidateCache(true); + invalidateCache(true, null); } - synchronized void doFlushCache(final IProgressMonitor monitor) throws CoreException { + synchronized void doFlushCache(final IProgressMonitor monitor, IPath[] toClean) throws CoreException { // nothing to be done if no information cached - if (getCacheState() == EMPTY_CACHE) + if (getCacheState() != INVALID_CACHE) return; - setCacheState(FLUSHING_CACHE); - // flush the MRU cache - cache.discardAll(); + 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 + MultiStatus result = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.INTERNAL_ERROR, Messages.resources_errorFlushingContentDescriptionCache, null); + for (int i = 0; i < toClean.length; i++) + try { + clearContentFlags(toClean[i], monitor); + } catch (CoreException ce) { + result.add(ce.getStatus()); + } + if (!result.isOK()) + throw new CoreException(result); + } + } 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) throws CoreException { // discard content type related flags for all files in the tree IElementContentVisitor visitor = new IElementContentVisitor() { public boolean visitElement(ElementTree tree, IPathRequestor requestor, Object elementContents) { @@ -180,12 +237,10 @@ } }; try { - new ElementTreeIterator(workspace.getElementTree(), Path.ROOT).iterate(visitor); + new ElementTreeIterator(workspace.getElementTree(), root).iterate(visitor); } catch (WrappedRuntimeException e) { throw (CoreException) e.getTargetException(); } - // done cleaning - setCacheState(EMPTY_CACHE); } Cache getCache() { @@ -288,7 +343,14 @@ return Platform.getPlatformAdmin().getState(false).getTimeStamp(); } - public synchronized void invalidateCache(boolean flush) { + /** + * 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; @@ -298,9 +360,8 @@ } catch (CoreException e) { ResourcesPlugin.getPlugin().getLog().log(e.getStatus()); } - if (!flush) - return; - flushJob.schedule(1000); + if (flush) + flushJob.flush(project); } /** @@ -310,8 +371,8 @@ // tries to obtain a description for this file contents InputStream contents = new LazyFileInputStream(file.getLocation()); try { - IContentTypeManager contentTypeManager = Platform.getContentTypeManager(); - return contentTypeManager.getDescriptionFor(contents, file.getName(), IContentDescription.ALL); + IContentTypeMatcher matcher = projectContentTypes.getMatcherFor((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); @@ -327,7 +388,22 @@ // no changes related to the content type registry if (event.getExtensionDeltas(Platform.PI_RUNTIME, PT_CONTENTTYPES).length == 0) return; - invalidateCache(true); + 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 : + case LifecycleEvent.PRE_PROJECT_CLOSE : + case LifecycleEvent.PRE_PROJECT_DELETE : + case LifecycleEvent.PRE_PROJECT_MOVE : + case LifecycleEvent.PRE_PROJECT_OPEN : + invalidateCache(true, (IProject) event.resource); + } } synchronized void setCacheState(byte newCacheState) throws CoreException { @@ -351,18 +427,23 @@ 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) // 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() != getPlatformTimeStamp()) - invalidateCache(false); + 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 Index: src/org/eclipse/core/internal/resources/NatureManager.java =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/NatureManager.java,v retrieving revision 1.23 diff -u -r1.23 NatureManager.java --- src/org/eclipse/core/internal/resources/NatureManager.java 21 Feb 2005 23:10:23 -0000 1.23 +++ src/org/eclipse/core/internal/resources/NatureManager.java 14 Apr 2005 15:34:17 -0000 @@ -322,10 +322,16 @@ * Returns the cached array of enabled natures for this project, * or null if there is nothing in the cache. */ - protected String[] getEnabledNatures(IProject project) { - if (natureEnablements != null) - return (String[]) natureEnablements.get(project); - return null; + protected String[] getEnabledNatures(Project project) { + String[] enabled; + if (natureEnablements != null) { + enabled = (String[]) natureEnablements.get(project); + if (enabled != null) + return enabled; + } + enabled = computeNatureEnablements(project); + setEnabledNatures(project, enabled); + return enabled; } /** @@ -425,10 +431,6 @@ */ public boolean isNatureEnabled(Project project, String id) { String[] enabled = getEnabledNatures(project); - if (enabled == null) { - enabled = computeNatureEnablements(project); - setEnabledNatures(project, enabled); - } for (int i = 0; i < enabled.length; i++) { if (enabled[i].equals(id)) return true; Index: src/org/eclipse/core/internal/resources/ProjectNatureDescriptor.java =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ProjectNatureDescriptor.java,v retrieving revision 1.13 diff -u -r1.13 ProjectNatureDescriptor.java --- src/org/eclipse/core/internal/resources/ProjectNatureDescriptor.java 21 Feb 2005 23:10:23 -0000 1.13 +++ src/org/eclipse/core/internal/resources/ProjectNatureDescriptor.java 14 Apr 2005 15:34:18 -0000 @@ -25,6 +25,7 @@ protected String[] requiredNatures; protected String[] natureSets; protected String[] builderIds; + protected String[] contentTypeIds; protected boolean allowLinking = true; //descriptors that are in a dependency cycle are never valid @@ -57,6 +58,14 @@ } /** + * Returns the IDs of the content types this nature declares to + * have affinity with. These content types do not necessarily exist in the registry. + */ + public String[] getContentTypeIds() { + return contentTypeIds; + } + + /** * @see IProjectNatureDescriptor#getNatureId() */ public String getNatureId() { @@ -106,6 +115,7 @@ ArrayList requiredList = new ArrayList(count); ArrayList setList = new ArrayList(count); ArrayList builderList = new ArrayList(count); + ArrayList contentTypeList = new ArrayList(count); for (int i = 0; i < count; i++) { IConfigurationElement element = elements[i]; String name = element.getName(); @@ -124,6 +134,11 @@ if (attribute == null) fail(); builderList.add(attribute); + } else if (name.equalsIgnoreCase("content-type")) { //$NON-NLS-1$ + String attribute = element.getAttribute("id"); //$NON-NLS-1$ + if (attribute == null) + fail(); + contentTypeList.add(attribute); } else if (name.equalsIgnoreCase("options")) { //$NON-NLS-1$ String attribute = element.getAttribute("allowLinking"); //$NON-NLS-1$ //when in doubt (missing attribute, wrong value) default to allow linking @@ -133,6 +148,7 @@ requiredNatures = (String[]) requiredList.toArray(new String[requiredList.size()]); natureSets = (String[]) setList.toArray(new String[setList.size()]); builderIds = (String[]) builderList.toArray(new String[builderList.size()]); + contentTypeIds = (String[]) contentTypeList.toArray(new String[contentTypeList.size()]); } /** Index: src/org/eclipse/core/internal/utils/Messages.java =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java,v retrieving revision 1.18 diff -u -r1.18 Messages.java --- src/org/eclipse/core/internal/utils/Messages.java 23 Feb 2005 20:08:21 -0000 1.18 +++ src/org/eclipse/core/internal/utils/Messages.java 14 Apr 2005 15:34:18 -0000 @@ -156,6 +156,7 @@ public static String resources_destNotNull; public static String resources_errorContentDescription; public static String resources_errorDeleting; + public static String resources_errorFlushingContentDescriptionCache; public static String resources_errorMarkersDelete; public static String resources_errorMarkersMove; public static String resources_errorMembers; Index: src/org/eclipse/core/internal/utils/Queue.java =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Queue.java,v retrieving revision 1.10 diff -u -r1.10 Queue.java --- src/org/eclipse/core/internal/utils/Queue.java 18 Mar 2005 17:27:48 -0000 1.10 +++ src/org/eclipse/core/internal/utils/Queue.java 14 Apr 2005 15:34:18 -0000 @@ -61,6 +61,23 @@ } tail = head = 0; } + + /** + * Copies all elements to the given array. Returns the same array as + * convenience. + */ + public Object[] copyTo(Object[] newElements) { + if (head <= tail) { + // trivial case + System.arraycopy(elements, head, newElements, 0, newElements.length); + return newElements; + } + // queue wrapped array - has to reassemble segments + int end = (elements.length - head); + System.arraycopy(elements, head, newElements, 0, end); + System.arraycopy(elements, 0, newElements, end, tail); + return newElements; + } /** * This method does not affect the queue itself. It is only a Index: src/org/eclipse/core/internal/utils/messages.properties =================================================================== RCS file: /home/eclipse/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties,v retrieving revision 1.107 diff -u -r1.107 messages.properties --- src/org/eclipse/core/internal/utils/messages.properties 23 Feb 2005 20:08:20 -0000 1.107 +++ src/org/eclipse/core/internal/utils/messages.properties 14 Apr 2005 15:34:18 -0000 @@ -112,6 +112,7 @@ resources_destNotNull = Destination path should not be null. resources_errorContentDescription = Error retrieving content description for resource: {0}. resources_errorDeleting = Error deleting resource {0} from the workspace tree. +resources_errorFlushingContentDescriptionCache = Errors occurred while flushing content description cache. resources_errorMarkersDelete = Error deleting markers for resource: {0}. resources_errorMarkersMove = Error moving markers from resource: {0} to: {1}. resources_errorMembers = Error retrieving members of container: {0}. Index: src/org/eclipse/core/internal/resources/ProjectContentTypes.java =================================================================== RCS file: src/org/eclipse/core/internal/resources/ProjectContentTypes.java diff -N src/org/eclipse/core/internal/resources/ProjectContentTypes.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/internal/resources/ProjectContentTypes.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) 2005 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Common Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/cpl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.resources; + +import java.util.*; +import org.eclipse.core.internal.utils.Cache; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.*; +import org.eclipse.core.runtime.content.IContentType; +import org.eclipse.core.runtime.content.IContentTypeMatcher; +import org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy; + +/** + * Manages project-specific content type behavior. + * + * @see ContentDescriptionManager + * @see ISelectionPolicy + * @since 3.1 + */ +public class ProjectContentTypes { + + /** + * A project-aware content type selection policy. + */ + private class ProjectContentTypeSelectionPolicy implements ISelectionPolicy { + + private Project project; + + public ProjectContentTypeSelectionPolicy(Project project) { + this.project = project; + } + + public IContentType[] select(IContentType[] candidates, boolean fileName, boolean content) { + return ProjectContentTypes.this.select(project, candidates, fileName, content); + } + + } + + private static final QualifiedName MATCHER_PROPERTY = new QualifiedName(ResourcesPlugin.PI_RESOURCES, "contentTypeMatcher"); //$NON-NLS-1$ + private Cache contentTypesPerProject; + private Workspace workspace; + + private static void swap(Object[] array, int i1, int i2) { + Object temp = array[i1]; + array[i1] = array[i2]; + array[i2] = temp; + } + + public ProjectContentTypes(Workspace workspace) { + this.workspace = workspace; + // keep cache small + this.contentTypesPerProject = new Cache(5, 30, 0.4); + } + + private Set getAssociatedContentTypes(Project project) { + final ResourceInfo info = project.getResourceInfo(false, false); + final String projectName = project.getName(); + synchronized (contentTypesPerProject) { + Cache.Entry entry = contentTypesPerProject.getEntry(projectName); + if (entry != null) + // we have an entry... + if (info != null && entry.getTimestamp() == info.getContentId()) + // ...and it is not stale, so just return it + return (Set) entry.getCached(); + // no cached information found, have to collect associated content types + Set result = collectAssociatedContentTypes(project); + if (entry == null) + // there was no entry before - create one + entry = contentTypesPerProject.addEntry(projectName, result, info.getContentId()); + else { + // just update the existing entry + entry.setTimestamp(info.getContentId()); + entry.setCached(result); + } + return result; + } + } + + IContentTypeMatcher getMatcherFor(Project project) throws CoreException { + //TODO is ProjectInfo a better place to keep the content type matcher? + IContentTypeMatcher matcher = (IContentTypeMatcher) project.getSessionProperty(MATCHER_PROPERTY); + if (matcher != null) + return matcher; + matcher = Platform.getContentTypeManager().getMatcher(new ProjectContentTypeSelectionPolicy(project), null); + project.setSessionProperty(MATCHER_PROPERTY, matcher); + return matcher; + } + + /** + * Collect content types associated to the natures configured for the given project. + */ + private Set collectAssociatedContentTypes(Project project) { + String[] enabledNatures = workspace.getNatureManager().getEnabledNatures(project); + if (enabledNatures.length == 0) + return Collections.EMPTY_SET; + Set related = new HashSet(enabledNatures.length); + for (int i = 0; i < enabledNatures.length; i++) { + ProjectNatureDescriptor descriptor = (ProjectNatureDescriptor) workspace.getNatureDescriptor(enabledNatures[i]); + if (descriptor == null) + // no descriptor found for the nature, skip it + continue; + String[] natureContentTypes = descriptor.getContentTypeIds(); + for (int j = 0; j < natureContentTypes.length; j++) + // collect associate content types + related.add(natureContentTypes[j]); + } + return related; + } + + /** + * Implements project specific, nature-based selection policy. No content types are vetoed. + * + * @see ISelectionPolicy + */ + final IContentType[] select(Project project, IContentType[] candidates, boolean fileName, boolean content) { + // since no vetoing is done here, don't go further if there is nothing to sort + if (candidates.length < 2) + return candidates; + final Set associated = getAssociatedContentTypes(project); + if (associated == null || associated.isEmpty()) + // project has no content types associated + return candidates; + // put content types that appear in related natures before those who don't + int relatedCount = 0; + for (int i = 0; i < candidates.length; i++) + if (associated.contains(candidates[i].getId())) { + if (relatedCount < i) + swap(candidates, i, relatedCount); + relatedCount++; + } + return candidates; + } +}