/*
* This software is Copyright 2005,2006,2007,2008 Langdale Consultants.
* Langdale Consultants can be contacted at: http://www.langdale.com.au
*/
package au.com.langdale.cimtoole.project;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.QualifiedName;
import au.com.langdale.cimtoole.CIMToolPlugin;
import au.com.langdale.kena.ModelFactory;
import au.com.langdale.kena.OntModel;
import au.com.langdale.util.Jobs;
import com.hp.hpl.jena.graph.compose.MultiUnion;
/**
* A cache of ontology models.
*/
public class Cache extends Info {
/**
* The session property used to cache models.
*/
public static final QualifiedName ONTMODEL = new QualifiedName(CIMToolPlugin.PLUGIN_ID, "ontmodel");
private ListenerList listeners = new ListenerList();
/**
* Clients provide this interface to be notified of cache changes.
*/
public interface CacheListener {
/**
* Indicates that a cached model is available. This occurs
* after parsing is complete, which is in turn triggered by
* the initial access to the model or a change in the underlying
* model file(s).
*
* @param key: the file or folder for the model
*/
public void modelCached(IResource key);
/**
* Indicates that a model has been removed from the cache
* because the model file has been deleted.
* @param key: the file for the model
*/
public void modelDropped(IResource key);
}
public Cache() {
ResourcesPlugin.getWorkspace().addResourceChangeListener(
new ResourceListener(), IResourceChangeEvent.POST_CHANGE);
}
/**
* Return a cached model, if cached. Otherwise initiate parsing to create and cache the model.
* @param file: the file for the model
* @return: the model cached for the given file
* or null if the model is not currently cached
* or the file is the wrong type as indicated by <code>Info.isParsable()</code>.
*/
public OntModel getOntology( IFile file ) {
if( ! isParseable(file))
return null;
return (OntModel) getCached(new ParseWorker(file, false));
}
/**
* Return the union of all models in a folder, if all are cached.
* Otherwise initiate parsing to create and cache the missing models.
* Each model corresponds to a file in the folder for which <code>Info.isParsable()</code> is true.
* @param folder: the folder containing model files.
* @return: the union of the cached models or null.
*/
public OntModel getMergedOntology( IFolder folder ) {
DynamicMergedModel merger = (DynamicMergedModel) getCached(new MergeWorker(folder, false));
if( merger == null )
return null;
return merger.getBaseModel();
}
/**
* Return a cached model, parse and cache the model first if necessary.
* @param file: the file for the model
* @return: the model
* @throws CoreException
*/
public OntModel getOntologyWait( IFile file ) throws CoreException {
if( ! isParseable(file))
throw error(file + " is not an ontology.");
return (OntModel) getCachedWait(new ParseWorker(file, false));
}
/**
* Return a union of cached models, parse and cache any missing models first if necessary.
* @param folder: the folder containing model files
* @return: the union of models
* @throws CoreException
*/
public OntModel getMergedOntologyWait( IFolder folder ) throws CoreException {
DynamicMergedModel merger = (DynamicMergedModel) getCachedWait(new MergeWorker(folder, false));
if( merger == null )
return null;
return merger.getBaseModel();
}
/**
* Register to receive cache change notifications.
* @param listener: the receiver.
*/
public void addCacheListener(CacheListener listener) {
listeners.add(listener);
}
/**
* Deregister to stop receiving cache notifications.
* @param listener: the receiver.
*/
public void removeCacheListener(CacheListener listener) {
listeners.remove(listener);
}
/**
* Inject a model into the cache (which may then be out of sync with the
* underlying file).
*/
public void setOntology(IFile resource, OntModel model ) {
try {
resource.setSessionProperty(ONTMODEL, model);
} catch (CoreException e) {
throw new RuntimeException(e);
}
fireModelCached(resource);
}
private Object getCached(CacheWorker worker) {
if( ! worker.getResource().exists())
return null;
try {
Object raw = worker.getCached();
if( raw != null)
return raw;
} catch (CoreException e) {
throw new RuntimeException(e);
}
runJob(worker);
return null;
}
private Object getCachedWait(CacheWorker worker) throws CoreException {
if( ! worker.getResource().exists())
return null;
Object raw = worker.getCached();
if( raw != null )
return raw;
Jobs.runWait(worker, worker.getResource());
return worker.getCached();
}
private void runJob(CacheWorker worker) {
Jobs.runJob(worker, worker.getResource(), "Parsing " + worker.getResource().getName());
}
private void fireModelCached(IResource key) {
Object[] current = listeners.getListeners();
for (int ix = 0; ix < current.length; ix++) {
CacheListener listener = (CacheListener) current[ix];
listener.modelCached(key);
}
}
private void fireModelDropped(IResource key) {
Object[] current = listeners.getListeners();
for (int ix = 0; ix < current.length; ix++) {
CacheListener listener = (CacheListener) current[ix];
listener.modelDropped(key);
}
}
private abstract class CacheWorker implements IWorkspaceRunnable {
private IResource resource;
private boolean update;
public CacheWorker(IResource resource, boolean update) {
this.resource = resource;
this.update = update;
}
public IResource getResource() {
return resource;
}
public void run(IProgressMonitor monitor) throws CoreException {
Object raw = getCached();
if( update && raw != null || ! update && raw == null ) {
Object model = build(resource, raw);
resource.setSessionProperty(ONTMODEL, model);
fireModelCached(resource);
}
}
public Object getCached() throws CoreException {
return resource.getSessionProperty(ONTMODEL);
}
protected abstract Object build(IResource resource, Object initial) throws CoreException;
}
private class ParseWorker extends CacheWorker {
public ParseWorker(IFile file, boolean update) {
super(file, update);
}
@Override
protected Object build(IResource resource, Object initial) throws CoreException {
return Task.parse((IFile)resource);
}
}
private class MergeWorker extends CacheWorker {
public MergeWorker(IFolder folder, boolean update) {
super(folder, update);
}
@Override
protected Object build(IResource resource, Object initial) throws CoreException {
DynamicMergedModel merger = new DynamicMergedModel((IFolder)resource);
if( initial != null) {
DynamicMergedModel old = (DynamicMergedModel) initial;
old.dispose();
}
return merger;
}
}
private class DynamicMergedModel implements CacheListener, IResourceVisitor {
private OntModel model;
private Map inputs = new HashMap();
private IFolder folder;
public DynamicMergedModel(IFolder folder) throws CoreException {
this.folder = folder;
if(folder.exists())
folder.accept(this);
addCacheListener(this);
model = createMergedModel();
}
public boolean visit(IResource raw) throws CoreException {
if( raw instanceof IFile ) {
IFile file = (IFile) raw;
if( isParseable(file)) {
OntModel input = getOntologyWait(file);
inputs.put(file, input);
}
}
return true;
}
public OntModel getBaseModel() {
return model;
}
public OntModel createMergedModel() {
MultiUnion union = new MultiUnion();
Iterator it = inputs.values().iterator();
while(it.hasNext()) {
OntModel input = (OntModel) it.next();
union.addGraph(input.getGraph());
}
return ModelFactory.createMem(union);
}
public void modelCached(IResource raw) {
if( ! (raw instanceof IFile))
return;
IFile key = (IFile) raw;
if( ! inputs.containsKey(key))
return;
OntModel input = getOntology(key);
if( input == null)
return;
inputs.put(key, input);
model = createMergedModel();
fireModelCached(folder);
}
public void modelDropped(IResource raw) {
if( ! (raw instanceof IFile))
return;
IFile key = (IFile) raw;
if( ! inputs.containsKey(key))
return;
inputs.remove(key);
model = createMergedModel();
fireModelCached(folder);
}
public void dispose() {
removeCacheListener(this);
}
}
private class ResourceListener implements IResourceChangeListener {
public void resourceChanged(IResourceChangeEvent event) {
if (event.getType() != IResourceChangeEvent.POST_CHANGE)
return;
DeltaVisitor visitor = new DeltaVisitor();
try {
event.getDelta().accept(visitor);
for (Iterator it = visitor.listProjects(); it.hasNext();) {
IProject project = (IProject) it.next();
IFolder schema = getSchemaFolder(project);
if( schema.exists())
runJob(new MergeWorker(schema, true));
}
} catch (CoreException e) {
throw new RuntimeException(e);
}
}
}
private class DeltaVisitor implements IResourceDeltaVisitor {
private final Set projects = new HashSet();;
public boolean visit(IResourceDelta delta) {
IResource raw = delta.getResource();
if( raw instanceof IFile ) {
IFile file = (IFile) raw;
if( isParseable(file)) {
IProject project = file.getProject();
int kind = delta.getKind();
if( kind == IResourceDelta.CHANGED) {
if((delta.getFlags()&(IResourceDelta.CONTENT|IResourceDelta.REPLACED)) != 0)
runJob(new ParseWorker(file, true));
}
else if( kind == IResourceDelta.ADDED ) {
projects.add(project);
}
else if( kind == IResourceDelta.REMOVED ) {
projects.add(project);
fireModelDropped(file);
}
}
else {
IFile master = findMasterFor(file);
if( master != null )
runJob(new ParseWorker(master, true));
}
return false;
}
else
return true;
}
public Iterator listProjects() { return projects.iterator(); }
}
}