blob: 1edb29d524b79b0d21fa19454ed4b89e45d1298a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009 SAP AG.
* 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:
* Eduard Bartsch (SAP AG) - initial API and implementation
* Mathias Kinzler (SAP AG) - initial API and implementation
*******************************************************************************/
package org.eclipse.core.resources.semantic.spi;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.resources.semantic.SemanticResourcesPlugin;
import org.eclipse.core.internal.resources.semantic.cacheservice.ICacheUpdateCallback;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.semantic.ISemanticFileSystem;
import org.eclipse.core.resources.semantic.SemanticResourceException;
import org.eclipse.core.resources.semantic.SemanticResourceStatusCode;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.osgi.util.NLS;
/**
* This will delegate calls to openInputStream and openOutputStream to local
* copies of the semantic files.
* <p>
* The local copies are created lazily as needed. If creation of such a copy is
* required, i.e. upon the first call to
* {@link #openInputStream(ISemanticFileStore, IProgressMonitor)}, the original
* data will be retrieved from the remote repository by a call to
* {@link #openInputStreamInternal(ISemanticFileStore, IProgressMonitor, ICacheTimestampSetter)}
* . Later calls to
* {@link #openInputStream(ISemanticFileStore, IProgressMonitor)} will be
* delegated to that copy.
* <p>
* By default, calls to
* {@link #openOutputStream(ISemanticFileStore, int, IProgressMonitor)} will
* open an {@link OutputStream} on the cached copy, i.e. only the cached copy
* will be updated. If the cached copy and the remote repository are to be kept
* in sync ("write-through"), it is possible to override
* {@link #onCacheUpdate(ISemanticFileStore, InputStream, long, boolean, IProgressMonitor)}
* and do the remote update there.
* <p>
* TODO 0.1: describe options for error handling for write-through scenario
* <p>
* This class is intended to be subclassed.
*
* @since 4.0
*
*/
public abstract class CachingContentProvider extends ContentProvider {
private static final QualifiedName RESOURCE_TIMESTAMP = new QualifiedName(SemanticResourcesPlugin.PLUGIN_ID, "ResourceTimestamp"); //$NON-NLS-1$
// TODO 0.1: add convenience/helper methods to cleanup cache along with
// file/folder removal
// TODO 0.1: add helpers for write-through error handling
/**
* Call back to set the timestamp of the cache.
* <p>
* This enables content providers to update the cache timestamp
*/
public final IDropCacheVisitor deleteAllVisitor = new IDropCacheVisitor() {
public boolean shouldDrop(ISemanticFileStore store) {
return store.getType() == ISemanticFileStore.FILE;
}
};
private ICacheService m_cacheService;
/**
* Call-back interface for setting the cache timestamp.
*
*/
public interface ICacheTimestampSetter {
/**
* sets the cache timestamp
*
* @param timestamp
* the timestamp to set for the cache entry
*/
public void setTimestamp(long timestamp);
/**
*
* @return the current timestamp
*/
public long getTimestamp();
}
/**
* A visitor used for dropping the cache.
*/
public interface IDropCacheVisitor {
/**
* @param store
* the semantic file store visited
* @return <code>true</code> if the store should be deleted
*/
public boolean shouldDrop(ISemanticFileStore store);
}
/**
*
* @return the cache service
* @throws CoreException
*/
public ICacheService getCacheService() throws CoreException {
if (this.m_cacheService == null) {
this.m_cacheService = this.getCacheServiceFactory().getCacheService();
}
return this.m_cacheService;
}
public final InputStream openInputStream(final ISemanticFileStore childStore, IProgressMonitor monitor) throws CoreException {
ICacheService cacheService = this.getCacheService();
IPath path = childStore.getPath();
if (!cacheService.hasContent(path)) {
ICacheTimestampSetter setter = new ICacheTimestampSetter() {
long lastTimestamp = 0;
public void setTimestamp(long timestamp) {
this.lastTimestamp = timestamp;
}
public long getTimestamp() {
return this.lastTimestamp;
}
};
InputStream is = null;
try {
is = openInputStreamInternal(childStore, monitor, setter);
cacheService.addContentWithTimestamp(path, is, setter.getTimestamp(), EFS.NONE, monitor);
} finally {
Util.safeClose(is);
}
}
return cacheService.getContent(path);
}
public final OutputStream openOutputStream(final ISemanticFileStore childStore, int options, final IProgressMonitor monitor)
throws CoreException {
ICacheService cacheService = this.getCacheService();
IPath path = childStore.getPath();
boolean appendMode = (options & ISemanticFileSystem.CONTENT_APPEND) > 0;
ICacheUpdateCallback callback = new ICacheUpdateCallback() {
public void cacheUpdated(InputStream newContent, long cacheTimestamp, boolean append) {
onCacheUpdate(childStore, newContent, cacheTimestamp, append, monitor);
}
};
return cacheService.wrapOutputStream(path, appendMode, callback, monitor);
}
/**
* Cache deletion; this corresponds to the deletion of an element.
* <p>
* Other than the
* {@link #dropCache(ISemanticFileStore, IProgressMonitor, IDropCacheVisitor, MultiStatus)}
* method, this will try to delete the cache ignoring the state of the cache
* file.
* <p>
* Recursively goes down the child hierarchy and tries do delete the
* corresponding cache entry.
*
* @param store
* @param monitor
* @throws CoreException
*/
protected void deleteCache(ISemanticFileStore store, IProgressMonitor monitor) throws CoreException {
MultiStatus status = new MultiStatus(SemanticResourcesPlugin.PLUGIN_ID, IStatus.OK, NLS.bind(
Messages.CachingContentProvider_DeletingCache_XMSG, store.getPath().toString()), null);
dropCache(store, monitor, this.deleteAllVisitor, status);
if (!status.isOK()) {
throw new CoreException(status);
}
}
/**
* Cache dropping; this corresponds to a refresh of an element.
* <p>
* Note that federated resources will <em>not</em> be visited.
* <p>
* Recursively goes down the child hierarchy and tries to delete the
* corresponding cache entry; it's up to the provided visitor to indicate
* whether a given cache entry should be deleted. In particular, cache
* entries pertaining to "locally changed" resources should not be dropped.
* <p>
*
* @param childStore
* the store
* @param monitor
* may be null
* @param visitor
* the visitor
* @param status
* the result status
*/
public void dropCache(ISemanticFileStore childStore, IProgressMonitor monitor, IDropCacheVisitor visitor, MultiStatus status) {
if (childStore.getType() == ISemanticFileStore.FILE) {
if (visitor.shouldDrop(childStore)) {
try {
ICacheService cacheService = this.getCacheService();
cacheService.removeContent(childStore.getPath(), monitor);
} catch (CoreException e) {
status.add(e.getStatus());
return;
}
}
} else {
try {
for (IFileStore store : childStore.childStores(EFS.NONE, monitor)) {
if (!(store instanceof ISemanticFileStore)) {
continue;
}
// check if we have federation
ISemanticFileStore sfs = (ISemanticFileStore) store;
String providerId = sfs.getContentProviderID();
// only if there is no federation, otherwise simply ignore
// since we don't know if the semantics of the visitor
// is correct for the federated provider
if (providerId == null) {
dropCache((ISemanticFileStore) store, monitor, visitor, status);
}
}
} catch (CoreException e) {
status.add(e.getStatus());
return;
}
}
}
/**
* Recursively goes down the hierarchy and fills the cache.
* <p>
* If the resource is writable, the cache will not be filled.
*
* @param semanticFileStore
* @param monitor
* @param status
*/
protected void fillCache(ISemanticFileStore semanticFileStore, IProgressMonitor monitor, MultiStatus status) {
if (semanticFileStore.getType() != ISemanticFileStore.FILE) {
try {
for (IFileStore store : semanticFileStore.childStores(EFS.NONE, monitor)) {
if (!(store instanceof ISemanticFileStore)) {
continue;
}
// check if we have federation
ISemanticFileStore sfs = (ISemanticFileStore) store;
String providerId = sfs.getContentProviderID();
if (providerId == null) {
fillCache((ISemanticFileStore) store, monitor, status);
} else {
try {
// delegation to other content provider
ISemanticContentProvider provider = sfs.getEffectiveContentProvider();
if (provider instanceof CachingContentProvider) {
((CachingContentProvider) provider).fillCache((ISemanticFileStore) store, monitor, status);
}
} catch (CoreException e) {
status.add(e.getStatus());
}
}
}
} catch (CoreException e) {
status.add(e.getStatus());
}
} else {
// in case of file
// local only content is always in cache
if (semanticFileStore.isLocalOnly()) {
return;
}
// if this is not read-only, we don't overwrite the timestamp
boolean readOnly;
try {
readOnly = fetchResourceInfo(semanticFileStore, ISemanticFileSystem.RESOURCE_INFO_READ_ONLY, monitor).isReadOnly();
} catch (CoreException e) {
status.add(e.getStatus());
return;
}
if (readOnly) {
// required to obtain the correct timestamp
try {
Util.safeClose(openInputStream(semanticFileStore, monitor));
} catch (CoreException e) {
status.add(e.getStatus());
}
}
}
}
/**
*
* @return the cache service factory
* @throws CoreException
*/
public abstract ICacheServiceFactory getCacheServiceFactory() throws CoreException;
/**
* To be implemented by concrete subclasses.
* <p>
* This method is called both from
* {@link #openInputStream(ISemanticFileStore, IProgressMonitor)} and from
* the default implementation of
* {@link #getResourceTimestamp(ISemanticFileStore, IProgressMonitor)}. This
* method allows retrieval of content and content timestamp with one
* roundtrip (e.g. via plain HTTP GET). But this approach has a drawback
* that, after adding new resources, all their content is retrieved on
* update of the resource tree.
* <p>
* Important: The timestamp that is obtained via {@code timeStampSetter}
* parameter is only used to set timestamp on cache service entry.
* {@link #setResourceTimestamp(ISemanticFileStore, long, IProgressMonitor)}
* method is not called by
* {@link #openInputStream(ISemanticFileStore, IProgressMonitor)}. This is
* done because the standard implementation of
* {@link #getResourceTimestamp(ISemanticFileStore, IProgressMonitor)} uses
* cache service to consistently store both content and timestamps in one
* place.
* <p>
* Content providers that are able to retrieve the resource timestamp
* independently from content, should override the methods
* {@link #getResourceTimestamp(ISemanticFileStore, IProgressMonitor)},
* {@link #setResourceTimestamp(ISemanticFileStore, long, IProgressMonitor)}
* and
* {@link #onCacheUpdate(ISemanticFileStore, InputStream, long, boolean, IProgressMonitor)}
* and provide an own timestamp handling that is independent from the cache
* service and allows lazy content retrieval. In this case, the value
* returned via timeStampSetter parameter is irrelevant and may be 0.
* <p>
* When implementing own timestamp handling, it is important to take into
* account that the method
* {@link #getResourceTimestamp(ISemanticFileStore, IProgressMonitor)} is
* invoked by {@link IResource#refreshLocal(int, IProgressMonitor)} so that
* the implementation of
* {@link #getResourceTimestamp(ISemanticFileStore, IProgressMonitor)} must
* not rely on
* {@link #openInputStreamInternal(ISemanticFileStore, IProgressMonitor, ICacheTimestampSetter)}
* being called before. It is also important to ensure that the timestamp is
* not changed unduly by
* {@link #openInputStreamInternal(ISemanticFileStore, IProgressMonitor, ICacheTimestampSetter)}
* because it will result in resource exceptions with error code
* {@link IResourceStatus#OUT_OF_SYNC_LOCAL} on subsequent access to
* resource content without calling
* {@link IResource#refreshLocal(int, IProgressMonitor)} in between.
*
* @param store
* the file store
* @param monitor
* may be null
* @param timeStampSetter
* a callback to report content timestamp
* @return the input stream
* @throws CoreException
* in case of failure
*/
public abstract InputStream openInputStreamInternal(ISemanticFileStore store, IProgressMonitor monitor,
ICacheTimestampSetter timeStampSetter) throws CoreException;
/**
* Notification about updated cache content.
* <p>
* After successful update of the cache, this method will be called; content
* providers should override this to update the remote repository.
* <p>
* The cache will <em>not</em> be rolled back upon failure of this method.
* The content provider is responsible to take appropriate action, e.g.
* re-synchronize the cache content. In order to keep track of the cache
* state, persistent properties could be used.
*
* @param semanticFileStore
* the semantic file store
* @param newContent
* the new cache content
* @param cacheTimestamp
* the timestamp as read from the cache
* @param append
* <code>true</code> to indicate that the cache was updated in
* append mode; in this case, only the appended data will be
* provided as new content
* @param monitor
* may be null
*/
public void onCacheUpdate(ISemanticFileStore semanticFileStore, InputStream newContent, long cacheTimestamp, boolean append,
IProgressMonitor monitor) {
// by default, we do nothing
}
public long getResourceTimestamp(ISemanticFileStore semanticFileStore, IProgressMonitor monitor) throws CoreException {
String stampString = semanticFileStore.getPersistentProperty(RESOURCE_TIMESTAMP);
if (stampString != null){
return Long.parseLong(stampString);
}
// the cache service can also give some information, but depending on the
// underlying file system, precision may be seconds, not milliseconds
ICacheService cacheService = this.getCacheService();
IPath path = semanticFileStore.getPath();
long timestamp = cacheService.getContentTimestamp(path);
if (timestamp >= 0) {
return timestamp;
}
// timestamp == -1 means that the content is not in the cache, so we try
// to get it
MultiStatus stat = new MultiStatus(SemanticResourcesPlugin.PLUGIN_ID, IStatus.OK, NLS.bind(
Messages.CachingContentProvider_FillCache_XGRP, semanticFileStore.getPath().toString()), null);
// ok since getResourceTimestamp is called only for files
fillCache(semanticFileStore, monitor, stat);
if (!stat.isOK()) {
throw new CoreException(stat);
}
timestamp = cacheService.getContentTimestamp(path);
if (timestamp >= 0) {
return timestamp;
}
throw new SemanticResourceException(SemanticResourceStatusCode.CACHED_CONTENT_NOT_FOUND, semanticFileStore.getPath(),
Messages.CachingContentProvider_TimestampNotInCache_XMSG);
}
public void setResourceTimestamp(ISemanticFileStore semanticFileStore, long timestamp, IProgressMonitor monitor) throws CoreException {
semanticFileStore.setPersistentProperty(RESOURCE_TIMESTAMP, Long.toString(timestamp));
// we also update the cache information, but this is additional information only
ICacheService cacheService = this.getCacheService();
IPath path = semanticFileStore.getPath();
if (cacheService.hasContent(path)) {
cacheService.setContentTimestamp(path, timestamp);
}
// TODO we probably don't want to get a cache update, do we?
// else {
// // ugly as it is, this will set the timestamp on the cache file
// // properly
// InputStream is = openInputStream(semanticFileStore, monitor);
// Util.safeClose(is);
// cacheService.setContentTimestamp(path, timestamp);
// }
}
}