/******************************************************************************* * Copyright (c) 2004, 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 * James Blackburn (Broadcom Corp.) - ongoing development *******************************************************************************/ 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<IPath> toFlush; private boolean fullFlush; public FlushJob() { super(Messages.resources_flushingContentDescriptionCache); setSystem(true); setUser(false); setPriority(LONG); setRule(workspace.getRoot()); toFlush = new ArrayList<IPath>(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 : 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 org.eclipse.core.runtime.content.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); } /** * Discovers, and caches, the content description of the requested File. * @param file to discover the content description for; result cached * @param info ResourceInfo for the passed in file * @param inSync boolean flag which indicates if cache can be trusted. If false false don't trust the cache * @return IContentDescription for the file * @throws CoreException */ public IContentDescription getDescriptionFor(File file, ResourceInfo info, boolean inSync) 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 (inSync && 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); } } if (inSync) { // tries to get a description from the cache synchronized (this) { 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 && inSync && 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(getTimestamp(info)); 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.POST_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); } }