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