blob: 0811591f7ff0a12f7ab62886fba5ab0c32c1a166 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2010 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.e4.core.services.internal.context;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.e4.core.services.IDisposable;
import org.eclipse.e4.core.services.context.ContextChangeEvent;
import org.eclipse.e4.core.services.context.EclipseContextFactory;
import org.eclipse.e4.core.services.context.IContextFunction;
import org.eclipse.e4.core.services.context.IEclipseContext;
import org.eclipse.e4.core.services.context.IRunAndTrack;
import org.eclipse.e4.core.services.context.spi.IContextConstants;
import org.eclipse.e4.core.services.context.spi.IEclipseContextStrategy;
import org.eclipse.e4.core.services.context.spi.ILookupStrategy;
import org.eclipse.e4.core.services.context.spi.ISchedulerStrategy;
/**
* This implementation assumes that all contexts are of the class EclipseContext. The external
* methods of it are exposed via IEclipseContext.
*/
public class EclipseContext implements IEclipseContext, IDisposable {
static class LookupKey {
Object[] arguments;
String name;
public LookupKey(String name, Object[] arguments) {
this.name = name;
this.arguments = arguments;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
LookupKey other = (LookupKey) obj;
if (!Arrays.equals(arguments, other.arguments))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result;
if (arguments != null) {
for (int i = 0; i < arguments.length; i++) {
Object arg = arguments[i];
result = prime * result + (arg == null ? 0 : arg.hashCode());
}
}
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
/**
* String representation for debugging purposes only.
*/
public String toString() {
return "Key(" + name + ',' + Arrays.asList(arguments) + ')'; //$NON-NLS-1$
}
}
static class TrackableComputationExt extends Computation implements IRunAndTrack {
private ContextChangeEvent cachedEvent;
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return 31 + ((runnable == null) ? 0 : runnable.hashCode());
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TrackableComputationExt other = (TrackableComputationExt) obj;
if (runnable == null) {
if (other.runnable != null)
return false;
} else if (!runnable.equals(other.runnable))
return false;
return true;
}
private IRunAndTrack runnable;
public TrackableComputationExt(IRunAndTrack runnable) {
this.runnable = runnable;
}
public Object getObject() {
if (runnable instanceof IRunAndTrackObject)
return ((IRunAndTrackObject) runnable).getObject();
return null;
}
final protected void doHandleInvalid(ContextChangeEvent event, List<Scheduled> scheduledList) {
int eventType = event.getEventType();
if (eventType == ContextChangeEvent.INITIAL || eventType == ContextChangeEvent.DISPOSE) {
// process right away
notify(event);
} else {
// schedule processing
scheduledList.add(new Scheduled(this, event));
}
}
public boolean notify(ContextChangeEvent event) {
// is this a structural event?
// structural changes: INITIAL, DISPOSE, UNINJECTED are always processed right away
int eventType = event.getEventType();
if ((runnable instanceof IRunAndTrackObject)
&& ((IRunAndTrackObject) runnable).batchProcess()) {
if ((eventType == ContextChangeEvent.ADDED)
|| (eventType == ContextChangeEvent.REMOVED)) {
cachedEvent = event;
EclipseContext eventsContext = (EclipseContext) event.getContext();
eventsContext.addWaiting(this);
// eventsContext.getRoot().waiting.add(this);
return true;
}
}
Computation oldComputation = currentComputation.get();
currentComputation.set(this);
boolean result = true;
try {
if (cachedEvent != null) {
result = runnable.notify(cachedEvent);
cachedEvent = null;
}
if (eventType != ContextChangeEvent.UPDATE)
result = runnable.notify(event);
} finally {
currentComputation.set(oldComputation);
}
EclipseContext eventsContext = (EclipseContext) event.getContext();
if (result)
startListening(eventsContext);
else
removeAll(eventsContext);
return result;
}
public String toString() {
return "TrackableComputationExt(" + runnable + ')'; //$NON-NLS-1$
}
}
static class Scheduled {
public IRunAndTrack runnable;
public ContextChangeEvent event;
public Scheduled(IRunAndTrack runnable, ContextChangeEvent event) {
this.runnable = runnable;
this.event = event;
}
public int hashCode() {
return 31 * (31 + event.hashCode()) + runnable.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Scheduled other = (Scheduled) obj;
if (!event.equals(other.event))
return false;
return runnable.equals(other.runnable);
}
}
static class DebugSnap {
Set<Computation> listeners = new HashSet<Computation>();
Map<LookupKey, ValueComputation> localValueComputations = Collections
.synchronizedMap(new HashMap<LookupKey, ValueComputation>());
Map<String, Object> localValues = Collections
.synchronizedMap(new HashMap<String, Object>());
}
static ThreadLocal<Computation> currentComputation = new ThreadLocal<Computation>();
// TODO replace with variable on bundle-specific class
public static boolean DEBUG = false;
public static boolean DEBUG_VERBOSE = false;
public static String DEBUG_VERBOSE_NAME = null;
private DebugSnap snapshot;
private static final Object[] NO_ARGUMENTS = new Object[0];
final Map<Computation, Computation> listeners = Collections
.synchronizedMap(new LinkedHashMap<Computation, Computation>());
final Map<LookupKey, ValueComputation> localValueComputations = Collections
.synchronizedMap(new HashMap<LookupKey, ValueComputation>());
final Map<String, Object> localValues = Collections
.synchronizedMap(new HashMap<String, Object>());
private final IEclipseContextStrategy strategy;
private ArrayList<String> modifiable;
private ArrayList<Computation> waiting; // list of Computations; null for all non-root entries
public EclipseContext(IEclipseContext parent, IEclipseContextStrategy strategy) {
this.strategy = strategy;
set(IContextConstants.PARENT, parent);
if (parent == null)
waiting = new ArrayList<Computation>();
}
public boolean containsKey(String name) {
return containsKey(name, false);
}
public boolean containsKey(String name, boolean localOnly) {
if (isSetLocally(name))
return true;
if (localOnly)
return false;
IEclipseContext parent = (IEclipseContext) getLocal(IContextConstants.PARENT);
if (parent != null && parent.containsKey(name))
return true;
if (strategy instanceof ILookupStrategy) {
if (((ILookupStrategy) strategy).containsKey(name, this))
return true;
}
return false;
}
/**
* Remember a snapshot of this context state for debugging purposes.
*/
public void debugSnap() {
snapshot = new DebugSnap();
snapshot.listeners = new HashSet<Computation>(listeners.keySet());
snapshot.localValueComputations = new HashMap<LookupKey, ValueComputation>(
localValueComputations);
snapshot.localValues = new HashMap<String, Object>(localValues);
}
/**
* Print a diff between the current context state and the last snapshot state
*/
public void debugDiff() {
if (snapshot == null)
return;
Set<Computation> listenerDiff = new HashSet<Computation>(listeners.keySet());
listenerDiff.removeAll(snapshot.listeners);
listenerDiff = new HashSet<Computation>(listenerDiff);// shrink the set
System.out.println("Listener diff (" + listenerDiff.size() + " leaked): "); //$NON-NLS-1$ //$NON-NLS-2$
for (Iterator<Computation> it = listenerDiff.iterator(); it.hasNext();) {
System.out.println("\t" + it.next()); //$NON-NLS-1$
}
Set<ValueComputation> computationDiff = new HashSet<ValueComputation>(
localValueComputations.values());
computationDiff.removeAll(snapshot.localValueComputations.values());
System.out.println("localValueComputations diff (" + computationDiff.size() + " leaked): "); //$NON-NLS-1$ //$NON-NLS-2$
for (Iterator<ValueComputation> it = computationDiff.iterator(); it.hasNext();) {
System.out.println("\t" + it.next()); //$NON-NLS-1$
}
Set<Object> valuesDiff = new HashSet<Object>(localValues.values());
valuesDiff.removeAll(snapshot.localValues.values());
System.out.println("localValues diff (" + valuesDiff.size() + " leaked): "); //$NON-NLS-1$ //$NON-NLS-2$
for (Iterator<Object> it = valuesDiff.iterator(); it.hasNext();) {
System.out.println("\t" + it.next()); //$NON-NLS-1$
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.e4.core.services.context.IEclipseContext#dispose()
*/
public void dispose() {
Computation[] ls = listeners.keySet().toArray(new Computation[listeners.size()]);
ContextChangeEvent event = EclipseContextFactory.createContextEvent(this,
ContextChangeEvent.DISPOSE, null, null, null);
// reverse order of listeners
for (int i = ls.length - 1; i >= 0; i--) {
List<Scheduled> scheduled = new ArrayList<Scheduled>();
ls[i].handleInvalid(event, scheduled);
processScheduled(scheduled);
}
// TBD used by OSGI Context strategy - is this needed? Looks like @PreDestroy
if (strategy instanceof IDisposable)
((IDisposable) strategy).dispose();
listeners.clear();
localValueComputations.clear();
localValues.clear();
}
public Object get(String name) {
return internalGet(this, name, NO_ARGUMENTS, false);
}
public Object get(String name, Object[] arguments) {
return internalGet(this, name, arguments, false);
}
public Object getLocal(String name) {
return internalGet(this, name, null, true);
}
public Object internalGet(EclipseContext originatingContext, String name, Object[] arguments,
boolean local) {
trackAccess(name);
if (DEBUG_VERBOSE) {
System.out.println("IEC.get(" + name + ", " + arguments + ", " + local + "):" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+ originatingContext + " for " + toString()); //$NON-NLS-1$
}
LookupKey lookupKey = null;
if (this == originatingContext) {
lookupKey = new LookupKey(name, arguments);
ValueComputation valueComputation = localValueComputations.get(lookupKey);
if (valueComputation != null) {
return valueComputation.get(arguments);
}
}
// 1. try for local value
Object result = localValues.get(name);
// 2. try the local strategy
if (result == null && strategy instanceof ILookupStrategy)
result = ((ILookupStrategy) strategy).lookup(name, originatingContext);
// if we found something, compute the concrete value and return
if (result != null) {
if (result instanceof IContextFunction) {
ValueComputation valueComputation = new ValueComputation(this, originatingContext,
name, ((IContextFunction) result));
if (EclipseContext.DEBUG)
System.out.println("created " + valueComputation); //$NON-NLS-1$
if (lookupKey == null)
lookupKey = new LookupKey(name, arguments);
originatingContext.localValueComputations.put(lookupKey, valueComputation);
// value computation depends on parent if function is defined in a parent
if (this != originatingContext)
valueComputation.addDependency(originatingContext, IContextConstants.PARENT);
result = valueComputation.get(arguments);
}
if (DEBUG_VERBOSE) {
System.out.println("IEC.get(" + name + "): " + result); //$NON-NLS-1$ //$NON-NLS-2$
}
return result;
}
// 3. delegate to parent
if (!local) {
IEclipseContext parent = (IEclipseContext) getLocal(IContextConstants.PARENT);
if (parent != null) {
return ((EclipseContext) parent).internalGet(originatingContext, name, arguments,
local); // XXX
// IEclipseContext
}
}
return null;
}
/**
* The given name has been modified or removed in this context. Invalidate all local value
* computations and listeners that depend on this name.
*/
public void invalidate(String name, int eventType, Object oldValue, List<Scheduled> scheduled) {
if (EclipseContext.DEBUG)
System.out.println("invalidating " + this + ',' + name); //$NON-NLS-1$
removeLocalValueComputations(name);
handleInvalid(name, eventType, oldValue, scheduled);
}
/**
* The value of the given name has changed in this context. This either means the value has been
* changed directly, or the value is a function that has been invalidated (one of the function's
* dependencies has changed).
*/
void handleInvalid(String name, int eventType, Object oldValue, List<Scheduled> scheduled) {
Computation[] ls = listeners.keySet().toArray(new Computation[listeners.size()]);
ContextChangeEvent event = EclipseContextFactory.createContextEvent(this, eventType, null,
name, oldValue);
for (int i = 0; i < ls.length; i++) {
ls[i].handleInvalid(event, scheduled);
}
}
private boolean isSetLocally(String name) {
trackAccess(name);
return localValues.containsKey(name);
}
public void remove(String name) {
if (isSetLocally(name)) {
Object oldValue = localValues.remove(name);
List<Scheduled> scheduled = new ArrayList<Scheduled>();
invalidate(name, ContextChangeEvent.REMOVED, oldValue, scheduled);
processScheduled(scheduled);
}
}
/**
* Removes all local value computations associated with the given name.
*
* @param name
* The name to remove
*/
private void removeLocalValueComputations(String name) {
synchronized (localValueComputations) {
// remove all keys with a matching name
for (Iterator<LookupKey> it = localValueComputations.keySet().iterator(); it.hasNext();) {
LookupKey key = it.next();
if (key.name.equals(name)) {
Object removed = localValueComputations.get(key);
if (removed instanceof ValueComputation) {
((ValueComputation) removed).clear(this, name);
}
it.remove();
}
}
}
}
public void runAndTrack(final IRunAndTrack runnable, Object[] args) {
ContextChangeEvent event = EclipseContextFactory.createContextEvent(this,
ContextChangeEvent.INITIAL, args, null, null);
TrackableComputationExt computation = new TrackableComputationExt(runnable);
computation.notify(event);
}
protected void processScheduled(List<Scheduled> scheduledList) {
boolean useScheduler = (strategy != null && strategy instanceof ISchedulerStrategy);
HashSet<Scheduled> sent = new HashSet<Scheduled>(scheduledList.size());
for (Iterator<Scheduled> i = scheduledList.iterator(); i.hasNext();) {
Scheduled scheduled = i.next();
// don't send the same event twice
if (!sent.add(scheduled))
continue;
if (useScheduler)
((ISchedulerStrategy) strategy).schedule(scheduled.runnable, scheduled.event);
else
scheduled.runnable.notify(scheduled.event);
}
}
public void set(String name, Object value) {
if (IContextConstants.PARENT.equals(name)) {
// TBD make setting parent a separate operation
List<Scheduled> scheduled = new ArrayList<Scheduled>();
handleReparent((EclipseContext) value, scheduled);
localValues.put(IContextConstants.PARENT, value);
processScheduled(scheduled);
return;
}
boolean containsKey = localValues.containsKey(name);
Object oldValue = localValues.put(name, value);
if (!containsKey || value != oldValue) {
if (DEBUG_VERBOSE) {
System.out.println("IEC.set(" + name + ',' + value + "):" + oldValue + " for " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ toString());
}
List<Scheduled> scheduled = new ArrayList<Scheduled>();
invalidate(name, ContextChangeEvent.ADDED, oldValue, scheduled);
processScheduled(scheduled);
}
}
public void modify(String name, Object value) {
List<Scheduled> scheduled = new ArrayList<Scheduled>();
if (!internalModify(name, value, scheduled))
set(name, value);
processScheduled(scheduled);
}
public boolean internalModify(String name, Object value, List<Scheduled> scheduled) {
boolean containsKey = localValues.containsKey(name);
if (containsKey) {
if (!checkModifiable(name)) {
String tmp = "Variable " + name + " is not modifiable in the context " + toString(); //$NON-NLS-1$ //$NON-NLS-2$
throw new IllegalArgumentException(tmp);
}
Object oldValue = localValues.put(name, value);
if (value != oldValue) {
if (DEBUG_VERBOSE)
System.out.println("IEC.set(" + name + ',' + value + "):" + oldValue + " for " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ toString());
invalidate(name, ContextChangeEvent.ADDED, oldValue, scheduled);
}
return true;
}
EclipseContext parent = getParent();
if (parent != null)
return parent.internalModify(name, value, scheduled);
return false;
}
// TBD should this method be an API?
public EclipseContext getParent() {
return (EclipseContext) localValues.get(IContextConstants.PARENT);
}
/**
* Returns a string representation of this context for debugging purposes only.
*/
public String toString() {
Object debugString = localValues.get(IContextConstants.DEBUG_STRING);
return debugString instanceof String ? ((String) debugString) : "Anonymous Context"; //$NON-NLS-1$
}
private void trackAccess(String name) {
Computation computation = currentComputation.get();
if (computation != null) {
computation.addDependency(this, name);
}
}
public void declareModifiable(String name) {
if (name == null)
return;
if (modifiable == null)
modifiable = new ArrayList<String>(3);
modifiable.add(name);
if (localValues.containsKey(name))
return;
localValues.put(name, null);
}
private boolean checkModifiable(String name) {
if (modifiable == null)
return false;
for (Iterator<String> i = modifiable.iterator(); i.hasNext();) {
String candidate = i.next();
if (candidate.equals(name))
return true;
}
return false;
}
public void removeListenersTo(Object object) {
if (object == null)
return;
Computation[] ls = listeners.keySet().toArray(new Computation[listeners.size()]);
ContextChangeEvent event = EclipseContextFactory.createContextEvent(this,
ContextChangeEvent.UNINJECTED, new Object[] { object }, null, null);
for (Computation computation : ls) {
((IRunAndTrack) computation).notify(event);
}
}
private void handleReparent(EclipseContext newParent, List<Scheduled> scheduled) {
// TBD should we lock waiting list while doing reparent?
// Add "boolean inReparent" on the root context and process right away?
processWaiting();
// 1) everybody who depends on me: I need to collect combined list of names injected
Computation[] ls = listeners.keySet().toArray(new Computation[listeners.size()]);
Set<String> usedNames = new HashSet<String>();
for (int i = 0; i < ls.length; i++) {
Set<String> listenerNames = ls[i].dependsOnNames(this);
if (listenerNames == null)
continue; // should not happen?
usedNames.addAll(listenerNames); // also removes duplicates
}
// 2) for each used name:
for (Iterator<String> i = usedNames.iterator(); i.hasNext();) {
String name = i.next();
if (localValues.containsKey(name))
continue; // it is a local value
Object oldValue = get(name);
Object newValue = (newParent != null) ? newParent.get(name) : null;
if (oldValue != newValue)
invalidate(name, ContextChangeEvent.ADDED, oldValue, scheduled);
}
localValueComputations.clear();
}
public void processWaiting() {
// traverse to the root node
EclipseContext parent = getParent();
if (parent != null) {
parent.processWaiting();
return;
}
if (waiting == null)
return;
// create update notifications
Computation[] ls = waiting.toArray(new Computation[waiting.size()]);
waiting.clear();
ContextChangeEvent event = EclipseContextFactory.createContextEvent(this,
ContextChangeEvent.UPDATE, null, null, null);
for (int i = 0; i < ls.length; i++) {
if (ls[i] instanceof TrackableComputationExt)
((TrackableComputationExt) ls[i]).notify(event);
}
}
public void addWaiting(Computation cp) {
// traverse to the root node
EclipseContext parent = getParent();
if (parent != null) {
parent.addWaiting(cp);
return;
}
if (waiting == null)
waiting = new ArrayList<Computation>();
waiting.add(cp);
}
protected EclipseContext getRoot() {
EclipseContext current = this;
EclipseContext root;
do {
root = current;
current = current.getParent();
} while (current != null);
return root;
}
}