blob: 764519df99d7a9d0e94d730d86c1069029c053ac [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 - 2005 University Of British Columbia 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:
* University Of British Columbia - initial API and implementation
*******************************************************************************/
package org.eclipse.mylar.core.internal;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.mylar.core.AbstractRelationProvider;
import org.eclipse.mylar.core.IMylarContext;
import org.eclipse.mylar.core.IMylarRelation;
import org.eclipse.mylar.core.IMylarContextListener;
import org.eclipse.mylar.core.IMylarElement;
import org.eclipse.mylar.core.IMylarStructureBridge;
import org.eclipse.mylar.core.InteractionEvent;
import org.eclipse.mylar.core.MylarPlugin;
import org.eclipse.mylar.core.util.IActiveTimerListener;
import org.eclipse.mylar.core.util.IInteractionEventListener;
import org.eclipse.ui.internal.Workbench;
/**
* This is the core class resposible for context management.
*
* @author Mik Kersten
*/
public class MylarContextManager {
private static final String ACTIVITY_DEACTIVATED = "deactivated";
private static final String ACTIVITY_ACTIVATED = "activated";
private static final String ACTIVITY_ID = "org.eclipse.mylar.core";
private static final String ACTIVITY_HANDLE = "attention";
private static final String ACTIVITY_KIND = "context";
public static final int ACTIVITY_TIMEOUT_MINUTES = 5; // in minutes
private static final String CONTEXT_HISTORY_FILE_NAME = "context-history";
public static final String SOURCE_ID_MODEL_PROPAGATION = "org.eclipse.mylar.core.model.interest.propagation";
public static final String SOURCE_ID_DECAY = "org.eclipse.mylar.core.model.interest.decay";
public static final String SOURCE_ID_DECAY_CORRECTION = "org.eclipse.mylar.core.model.interest.decay.correction";
public static final String SOURCE_ID_MODEL_ERROR = "org.eclipse.mylar.core.model.interest.propagation";
public static final String CONTAINMENT_PROPAGATION_ID = "org.eclipse.mylar.core.model.edges.containment";
public static final String FILE_EXTENSION = ".xml";
private static final int MAX_PROPAGATION = 17; // TODO: parametrize this
private int numInterestingErrors = 0;
private List<String> errorElementHandles = new ArrayList<String>();
private CompositeContext activeContext = new CompositeContext();
private MylarContext activityHistory = null;
private ActivityListener activityListener;
private List<IMylarContextListener> listeners = new ArrayList<IMylarContextListener>();
private List<IMylarContextListener> waitingListeners = new ArrayList<IMylarContextListener>();
// TODO: move
private List<IActionExecutionListener> actionExecutionListeners = new ArrayList<IActionExecutionListener>();
private boolean suppressListenerNotification = false;
private MylarContextExternalizer externalizer = new MylarContextExternalizer();
private boolean activationHistorySuppressed = false;
private static ScalingFactors scalingFactors = new ScalingFactors();
private class ActivityListener implements IActiveTimerListener, IInteractionEventListener {
private ActivityTimerThread timer;
private boolean isStalled;
public ActivityListener(){
timer = new ActivityTimerThread(ACTIVITY_TIMEOUT_MINUTES);
timer.addListener(this);
timer.start();
MylarPlugin.getDefault().addInteractionListener(this);
}
public void fireTimedOut() {
if (!isStalled) {
activityHistory.parseEvent(
new InteractionEvent(InteractionEvent.Kind.COMMAND,
ACTIVITY_KIND,
ACTIVITY_HANDLE,
ACTIVITY_ID,
null,
ACTIVITY_DEACTIVATED,
1f));
}
isStalled = true;
timer.resetTimer();
}
public void interactionObserved(InteractionEvent event) {
timer.resetTimer();
if(isStalled) {
activityHistory.parseEvent(
new InteractionEvent(InteractionEvent.Kind.COMMAND,
ACTIVITY_KIND,
ACTIVITY_HANDLE,
ACTIVITY_ID,
null,
ACTIVITY_ACTIVATED,
1f));
}
isStalled = false;
}
public void start() {}
public void stopTimer() {
timer.killThread();
MylarPlugin.getDefault().removeInteractionListener(this);
}
public void stop() {}
}
public MylarContextManager() {
File storeDir = new File(MylarPlugin.getDefault().getMylarDataDirectory());
storeDir.mkdirs();
activityHistory = externalizer.readContextFromXML(getFileForContext(CONTEXT_HISTORY_FILE_NAME));
if (activityHistory == null) {
resetActivityHistory();
}
activityListener = new ActivityListener();//ACTIVITY_TIMEOUT_MINUTES);
activityListener.start();
// activityTimer.addListener(new IActiveTimerListener() {
// public void fireTimedOut() {
//
// }
// });
// activityTimer.start();
}
public IMylarElement getActiveNode() {
if (activeContext != null) {
return activeContext.getActiveNode();
} else {
return null;
}
}
public void addErrorPredictedInterest(String handle, String kind, boolean notify) {
if (numInterestingErrors > scalingFactors.getMaxNumInterestingErrors()
|| activeContext.getContextMap().isEmpty()) return;
InteractionEvent errorEvent = new InteractionEvent(
InteractionEvent.Kind.PROPAGATION,
kind, handle,
SOURCE_ID_MODEL_ERROR,
scalingFactors.getErrorInterest());
handleInteractionEvent(errorEvent, true);
errorElementHandles.add(handle);
numInterestingErrors++;
}
/**
* TODO: worry about decay-related change if predicted interest dacays
*/
public void removeErrorPredictedInterest(String handle, String kind, boolean notify) {
if (activeContext.getContextMap().isEmpty()) return;
if (handle == null) return;
IMylarElement node = activeContext.get(handle);
if (node != null
&& node.getDegreeOfInterest().isInteresting()
&& errorElementHandles.contains(handle)) {
InteractionEvent errorEvent = new InteractionEvent(
InteractionEvent.Kind.MANIPULATION,
kind, handle,
SOURCE_ID_MODEL_ERROR,
-scalingFactors.getErrorInterest());
handleInteractionEvent(errorEvent, true);
numInterestingErrors--;
errorElementHandles.remove(handle);
// TODO: this results in double-notification
if (notify) for (IMylarContextListener listener : listeners) listener.interestChanged(node);
}
}
public IMylarElement getElement(String elementHandle) {
if (activeContext != null) {
return activeContext.get(elementHandle);
} else {
return null;
}
}
public IMylarElement handleInteractionEvent(InteractionEvent event) {
return handleInteractionEvent(event, true);
}
public IMylarElement handleInteractionEvent(InteractionEvent event, boolean propagateToParents) {
return handleInteractionEvent(event, true, true);
}
/**
* TODO: consider moving this into the context?
*/
public IMylarElement handleInteractionEvent(InteractionEvent event, boolean propagateToParents, boolean notifyListeners) {
if (event.getKind() == InteractionEvent.Kind.COMMAND) return null;
if (activeContext.getContextMap().values().size() == 0) return null;
if (suppressListenerNotification) return null;
IMylarElement previous = activeContext.get(event.getStructureHandle());
float previousInterest = 0;
boolean previouslyPredicted = false;
boolean previouslyPropagated = false;
float decayOffset = 0;
if (previous != null) {
previousInterest = previous.getDegreeOfInterest().getValue();
previouslyPredicted = previous.getDegreeOfInterest().isPropagated();
previouslyPropagated = previous.getDegreeOfInterest().isPropagated();
}
if (event.getKind().isUserEvent()) {
if (previousInterest < 0) { // reset interest if not interesting
decayOffset = (-1)*(previous.getDegreeOfInterest().getValue());
activeContext.addEvent(new InteractionEvent(
InteractionEvent.Kind.MANIPULATION,
event.getContentType(),
event.getStructureHandle(),
SOURCE_ID_DECAY_CORRECTION,
decayOffset));
}
}
IMylarElement node = activeContext.addEvent(event);
List<IMylarElement> interestDelta = new ArrayList<IMylarElement>();
if (propagateToParents && !event.getKind().equals(InteractionEvent.Kind.MANIPULATION)) {
propegateDoiToParents(node, previousInterest, decayOffset, 1, interestDelta);
}
if (event.getKind().isUserEvent()) activeContext.setActiveElement(node);
if (isInterestDelta(previousInterest, previouslyPredicted, previouslyPropagated, node)) {
interestDelta.add(node); // TODO: check that the order of these is sensible
}
if (notifyListeners && !interestDelta.isEmpty()) {
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) {
listener.interestChanged(interestDelta);
}
}
checkForLandmarkDeltaAndNotify(previousInterest, node);
return node;
}
private boolean isInterestDelta(float previousInterest, boolean previouslyPredicted, boolean previouslyPropagated, IMylarElement node) {
float currentInterest = node.getDegreeOfInterest().getValue();
if (previousInterest <= 0 && currentInterest > 0) {
return true;
} else if (previousInterest > 0 && currentInterest <=0){
return true;
} else if (currentInterest > 0 && previouslyPredicted && !node.getDegreeOfInterest().isPredicted()) {
return true;
} else if (currentInterest > 0 && previouslyPropagated && !node.getDegreeOfInterest().isPropagated()) {
return true;
} else {
return false;
}
}
private void checkForLandmarkDeltaAndNotify(float previousInterest, IMylarElement node) {
// TODO: don't call interestChanged if it's a landmark?
IMylarStructureBridge bridge = MylarPlugin.getDefault().getStructureBridge(node.getContentType());
if (bridge.canBeLandmark(node.getHandleIdentifier())) {
if (previousInterest >= scalingFactors.getLandmark() && !node.getDegreeOfInterest().isLandmark()) {
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) listener.landmarkRemoved(node);
} else if (previousInterest < scalingFactors.getLandmark() && node.getDegreeOfInterest().isLandmark()) {
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) listener.landmarkAdded(node);
}
}
}
private void propegateDoiToParents(IMylarElement node, float previousInterest, float decayOffset, int level, List<IMylarElement> elementDelta) {
if (level > MAX_PROPAGATION || node == null || node.getDegreeOfInterest().getValue() <= 0) return;// || "/".equals(node.getElementHandle())) return;
checkForLandmarkDeltaAndNotify(previousInterest, node);
level++; // original is 1st level
float propagatedIncrement = node.getDegreeOfInterest().getValue() - previousInterest + decayOffset;
// float propagatedIncrement = scalingFactors.getParentPropagationIncrement(level);
IMylarStructureBridge adapter = MylarPlugin.getDefault().getStructureBridge(node.getContentType());
String parentHandle = adapter.getParentHandle(node.getHandleIdentifier());
if (parentHandle != null) {
InteractionEvent propagationEvent = new InteractionEvent(
InteractionEvent.Kind.PROPAGATION,
adapter.getContentType(node.getHandleIdentifier()),
adapter.getParentHandle(node.getHandleIdentifier()),
SOURCE_ID_MODEL_PROPAGATION,
CONTAINMENT_PROPAGATION_ID,
propagatedIncrement);
IMylarElement previous = activeContext.get(propagationEvent.getStructureHandle());
if (previous != null && previous.getDegreeOfInterest() != null) previousInterest = previous.getDegreeOfInterest().getValue();
CompositeContextElement parentNode = (CompositeContextElement)activeContext.addEvent(propagationEvent);
if (isInterestDelta(
previousInterest,
previous.getDegreeOfInterest().isPredicted(),
previous.getDegreeOfInterest().isPropagated(),
parentNode)) {
elementDelta.add(0, parentNode);
}
propegateDoiToParents(parentNode, previousInterest, decayOffset, level, elementDelta);//adapter.getResourceExtension(), adapter.getParentHandle(parentHandle), level, doi, parentChain);
}
}
public List<IMylarElement> findCompositesForNodes(List<MylarContextElement> nodes) {
List<IMylarElement> composites = new ArrayList<IMylarElement>();
for (MylarContextElement node : nodes) {
composites.add(activeContext.get(node.getHandleIdentifier()));
}
return composites;
}
public void addListener(IMylarContextListener listener) {
if (listener != null) {
if (suppressListenerNotification) {
waitingListeners.add(listener);
} else {
listeners.add(listener);
}
} else {
MylarPlugin.log("attempted to add null lisetener", this);
}
}
public void removeListener(IMylarContextListener listener) {
listeners.remove(listener);
}
public void removeAllListeners() {
listeners.clear();
}
public void notifyPostPresentationSettingsChange(IMylarContextListener.UpdateKind kind) {
for (IMylarContextListener listener : listeners) listener.presentationSettingsChanged(kind);
}
public void notifyActivePresentationSettingsChange(IMylarContextListener.UpdateKind kind) {
for (IMylarContextListener listener : listeners) listener.presentationSettingsChanging(kind);
}
/**
* For testing
*/
public void contextActivated(MylarContext context) {
activeContext.getContextMap().put(context.getId(), context);
if (!activationHistorySuppressed) {
activityHistory.parseEvent(
new InteractionEvent(InteractionEvent.Kind.COMMAND,
ACTIVITY_KIND,
context.getId(),
ACTIVITY_ID,
null,
ACTIVITY_ACTIVATED,
1f));
}
}
public void contextActivated(String id, String path) {
try {
suppressListenerNotification = true;
MylarContext context = activeContext.getContextMap().get(id);
if (context == null) context = loadContext(id, path);
if (context != null) {
contextActivated(context);
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) {
listener.contextActivated(context);
}
refreshRelatedElements();
} else {
MylarPlugin.log("Could not load context", this);
}
suppressListenerNotification = false;
listeners.addAll(waitingListeners);
} catch (Throwable t) {
MylarPlugin.log(t, "Could not activate context");
}
}
/**
* Could load in the context and inspect it, but this is cheaper.
*/
public boolean hasContext(String path) {
File contextFile = getFileForContext(path);
return contextFile.exists() && contextFile.length() > 0;
}
/**
* @param id
*/
public void contextDeactivated(String id, String path) {
try {
IMylarContext context = activeContext.getContextMap().get(id);
if (context != null) {
saveContext(id, path);
activeContext.getContextMap().remove(id);
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) {
listener.contextDeactivated(context);
}
}
if (!activationHistorySuppressed) {
activityHistory.parseEvent(
new InteractionEvent(InteractionEvent.Kind.COMMAND,
ACTIVITY_KIND,
id,
ACTIVITY_ID,
null,
ACTIVITY_DEACTIVATED,
1f));
}
saveActivityHistoryContext();
} catch (Throwable t) {
MylarPlugin.log(t, "Could not deactivate context");
}
}
public void contextDeleted(String id, String path) {
IMylarContext context = activeContext.getContextMap().get(id);
eraseContext(id, false);
if (context != null) { // TODO: this notification is redundant with eraseContext's
for (IMylarContextListener listener : listeners) listener.contextDeactivated(context);
}
try {
File f = getFileForContext(path);
if (f.exists()) {
f.delete();
}
} catch (SecurityException e) {
MylarPlugin.fail(e, "Could not delete context file", false);
}
}
private void eraseContext(String id, boolean notify) {
MylarContext context = activeContext.getContextMap().get(id);
if (context == null) return;
activeContext.getContextMap().remove(context);
context.reset();
if (notify) {
for (IMylarContextListener listener : listeners) listener.presentationSettingsChanging(IMylarContextListener.UpdateKind.UPDATE);
}
}
/**
* @return false if the map could not be read for any reason
*/
public MylarContext loadContext(String id, String path) {
MylarContext loadedContext = externalizer.readContextFromXML(getFileForContext(path));
if (loadedContext == null) {
return new MylarContext(id, MylarContextManager.getScalingFactors());
} else {
return loadedContext;
}
}
public void saveContext(String id, String path) {
MylarContext context = activeContext.getContextMap().get(id);
if (context == null) return;
context.collapse();
externalizer.writeContextToXML(context, getFileForContext(path));
}
private void saveActivityHistoryContext() {
externalizer.writeContextToXML(activityHistory, getFileForContext(CONTEXT_HISTORY_FILE_NAME));
}
public File getFileForContext(String path) {
return new File(MylarPlugin.getDefault().getMylarDataDirectory() + File.separator + path + FILE_EXTENSION);
}
public IMylarContext getActiveContext() {
return activeContext;
}
/**
* @param kind
*/
public void resetLandmarkRelationshipsOfKind(String reltationKind) {
for (IMylarElement landmark : activeContext.getLandmarks()) {
for (IMylarRelation edge : landmark.getRelations()) {
if (edge.getRelationshipHandle().equals(reltationKind)) {
landmark.clearEdges(); }
}
}
for (IMylarContextListener listener : listeners) listener.edgesChanged(null);
}
/**
* Copy the listener list in case it is modified during the notificiation.
* @param node
*/
public void notifyRelationshipsChanged(IMylarElement node) {
if (suppressListenerNotification) return;
for (IMylarContextListener listener : new ArrayList<IMylarContextListener>(listeners)) {
listener.edgesChanged(node);
}
}
public void refreshRelatedElements() {
for(IMylarStructureBridge bridge: MylarPlugin.getDefault().getStructureBridges().values()){
if(bridge.getRelationshipProviders() != null){
for (AbstractRelationProvider provider : bridge.getRelationshipProviders()) {
List<AbstractRelationProvider> providerList = new ArrayList<AbstractRelationProvider>();
providerList.add(provider);
updateDegreesOfSeparation(providerList, provider.getCurrentDegreeOfSeparation());
// if (provider.isEnabled()) {
// MylarPlugin.getContextManager().resetLandmarkRelationshipsOfKind(provider.getId());
// }
// for (IMylarElement node : activeContext.getLandmarks()) provider.landmarkAdded(node);
}
}
}
}
public List<AbstractRelationProvider> getActiveRelationProviders() {
List<AbstractRelationProvider> providers = new ArrayList<AbstractRelationProvider>();
Map<String, IMylarStructureBridge> bridges = MylarPlugin.getDefault().getStructureBridges();
for (String extension : bridges.keySet()) {
IMylarStructureBridge bridge = bridges.get(extension);
providers.addAll(bridge.getRelationshipProviders());
}
return providers;
}
public void updateDegreeOfSeparation(AbstractRelationProvider provider, int degreeOfSeparation) {
MylarPlugin.getContextManager().resetLandmarkRelationshipsOfKind(provider.getId());
if (degreeOfSeparation <= 0) {
// provider.setEnabled(false);
provider.setDegreeOfSeparation(degreeOfSeparation);
} else {
// provider.setEnabled(true);
provider.setDegreeOfSeparation(degreeOfSeparation);
for (IMylarElement node : activeContext.getLandmarks()) provider.landmarkAdded(node);
}
}
public void updateDegreesOfSeparation(List<AbstractRelationProvider> providers, int degreeOfSeparation) {
for(AbstractRelationProvider provider: providers) {
updateDegreeOfSeparation(provider, degreeOfSeparation);
}
}
public static ScalingFactors getScalingFactors() {
return MylarContextManager.scalingFactors;
}
public void updateMylarDirContents(String prevDir) {
File prev = new File(prevDir);
if (!prev.isDirectory()) {
return;
}
File[] contents = prev.listFiles();
File curr = new File(MylarPlugin.getDefault().getMylarDataDirectory());
for (File f : contents) {
// XXX: remove hack below
if ( (f.getName().endsWith(".xml") && f.getName().startsWith("task")) || f.getName().startsWith("mylar")) {
String name = curr.getAbsolutePath() + "/" + f.getName();
f.renameTo(new File(name));
}
}
}
public boolean hasActiveContext() {
return activeContext.getContextMap().values().size() > 0;
}
public List<IMylarElement> getActiveLandmarks() {
List<IMylarElement> allLandmarks = activeContext.getLandmarks();
List<IMylarElement> acceptedLandmarks = new ArrayList<IMylarElement>();
for (IMylarElement node : allLandmarks) {
IMylarStructureBridge bridge = MylarPlugin.getDefault().getStructureBridge(node.getContentType());
if (bridge.canBeLandmark(node.getHandleIdentifier())) {
acceptedLandmarks.add(node);
}
}
return acceptedLandmarks;
}
public Set<IMylarElement> getInterestingResources(IMylarContext context) {
Set<IMylarElement> interestingFiles = new HashSet<IMylarElement>();
List<IMylarElement> allIntersting = context.getInteresting();
for (IMylarElement node : allIntersting) {
if (MylarPlugin.getDefault().getStructureBridge(node.getContentType()).isDocument(node.getHandleIdentifier())) {
interestingFiles.add(node);
}
}
return interestingFiles;
}
/**
* Get the interesting resources for the active context.
*/
public Set<IMylarElement> getActiveContextResources() {
return getInterestingResources(activeContext);
}
public void actionObserved(IAction action, String info) {
for (IActionExecutionListener listener : actionExecutionListeners) {
listener.actionObserved(action);
}
}
public List<IActionExecutionListener> getActionExecutionListeners() {
return actionExecutionListeners;
}
public MylarContext getActivityHistory() {
return activityHistory;
}
public void resetActivityHistory() {
activityHistory = new MylarContext(CONTEXT_HISTORY_FILE_NAME, MylarContextManager.getScalingFactors());
saveActivityHistoryContext();
}
public boolean isActivationHistorySuppressed() {
return activationHistorySuppressed;
}
public void setActivationHistorySuppressed(boolean activationHistorySuppressed) {
this.activationHistorySuppressed = activationHistorySuppressed;
}
public void manipulateInterestForNode(IMylarElement node, boolean increment, boolean forceLandmark, String sourceId) {
float originalValue = node.getDegreeOfInterest().getValue();
float changeValue = 0;
if (!increment) {
if (node.getDegreeOfInterest().isLandmark()) { // keep it interesting
changeValue = (-1 * originalValue) + 1;
} else {
if (originalValue >=0) changeValue = (-1 * originalValue)-1;
}
} else {
if (!forceLandmark && (originalValue > MylarContextManager.getScalingFactors().getLandmark())) {
changeValue = 0;
} else { // make it a landmark
IMylarStructureBridge bridge = MylarPlugin.getDefault().getStructureBridge(node.getContentType());
if (node != null
&& bridge.canBeLandmark(node.getHandleIdentifier())
&& !bridge.getContentType(node.getHandleIdentifier()).equals(MylarPlugin.CONTENT_TYPE_ANY)) {
changeValue = MylarContextManager.getScalingFactors().getLandmark() - originalValue + 1;
} else {
// TODO: move this to UI?
MessageDialog.openInformation(
Workbench.getInstance().getActiveWorkbenchWindow().getShell(),
"Mylar Interest Manipulation",
"This element is not a valid landmark because it is not a structured element. Note that files and other resources can not be landmarks.");
}
}
}
if (changeValue != 0) {
InteractionEvent interactionEvent = new InteractionEvent(
InteractionEvent.Kind.MANIPULATION,
node.getContentType(),
node.getHandleIdentifier(),
sourceId,
changeValue);
MylarPlugin.getContextManager().handleInteractionEvent(interactionEvent);
}
}
public void setActiveSearchEnabled(boolean enabled) {
for(AbstractRelationProvider provider: getActiveRelationProviders()){
provider.setEnabled(enabled);
}
}
/**
* Retruns the highest interet context
*/
public String getDominantContextHandleForElement(IMylarElement node) {
IMylarElement dominantNode = null;
if (node instanceof CompositeContextElement) {
CompositeContextElement compositeNode = (CompositeContextElement)node;
if (compositeNode.getNodes().isEmpty()) return null;
dominantNode = (IMylarElement)compositeNode.getNodes().toArray()[0];
for(IMylarElement concreteNode : compositeNode.getNodes()) {
if (dominantNode != null
&& dominantNode.getDegreeOfInterest().getValue() < concreteNode.getDegreeOfInterest().getValue()) {
dominantNode = concreteNode;
}
}
} else if (node instanceof MylarContextElement) {
dominantNode = node;
}
if (node != null) {
return ((MylarContextElement)dominantNode).getContext().getId();
} else {
return null;
}
}
}