blob: 5a0f644452b122b99793ace0ecba10f8b21e72fd [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Daniel Kruegler <daniel.kruegler@gmail.com> - Bug 487417
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 492963
*******************************************************************************/
package org.eclipse.e4.core.internal.contexts;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import org.eclipse.e4.core.contexts.IContextFunction;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.contexts.RunAndTrack;
import org.eclipse.e4.core.di.IInjector;
import org.eclipse.e4.core.internal.contexts.osgi.ContextDebugHelper;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
/**
* 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 {
/**
* A context key identifying the parent context.
*/
public static final String PARENT = "parentContext"; //$NON-NLS-1$
/**
* A context key (value "debugString") identifying a value to use in debug statements for a
* context. A computed value can be used to embed more complex information in debug statements.
*/
public static final String DEBUG_STRING = "debugString"; //$NON-NLS-1$
/**
* String used in {@link #toString()} to name contexts not providing the value
* for the {@link #DEBUG_STRING} variable
*/
public static final String ANONYMOUS_CONTEXT_NAME = "Anonymous Context"; //$NON-NLS-1$
static class Scheduled {
public TrackableComputationExt runnable;
public ContextChangeEvent event;
public Scheduled(TrackableComputationExt runnable, ContextChangeEvent event) {
this.runnable = runnable;
this.event = event;
}
@Override
public int hashCode() {
return 31 * (31 + event.hashCode()) + runnable.hashCode();
}
@Override
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;
return Objects.equals(this.event, other.event) && Objects.equals(this.runnable, other.runnable);
}
}
private WeakGroupedListenerList weakListeners = new WeakGroupedListenerList();
private Map<String, ValueComputation> localValueComputations = Collections.synchronizedMap(new HashMap<>());
final protected Map<String, Object> localValues = Collections.synchronizedMap(new HashMap<>());
private Set<String> modifiable;
private List<Computation> waiting; // list of Computations; null for all non-root entries
private Set<WeakReference<EclipseContext>> children = new HashSet<>();
private Set<IContextDisposalListener> notifyOnDisposal = new HashSet<>();
static private ThreadLocal<Stack<Computation>> currentComputation = new ThreadLocal<>();
// I don't think we need to sync referenceQueue access
private ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
private Map<Reference<?>, TrackableComputationExt> activeComputations = Collections.synchronizedMap(new HashMap<>());
private Set<TrackableComputationExt> activeRATs = Collections.synchronizedSet(new HashSet<>());
private final static Object[] nullArgs = new Object[] {null};
/**
* A context key (value "activeChildContext") that identifies another {@link IEclipseContext}
* that is a child of the context. The meaning of active is up to the application.
*/
public static final String ACTIVE_CHILD = "activeChildContext"; //$NON-NLS-1$
static private final IEclipseContextDebugger debugAddOn = ContextDebugHelper.getDebugger();
public EclipseContext(IEclipseContext parent) {
setParent(parent);
if (parent == null)
waiting = Collections.synchronizedList(new ArrayList<>());
if (debugAddOn != null)
debugAddOn.notify(this, IEclipseContextDebugger.EventType.CONSTRUCTED, null);
}
final static private Set<EclipseContext> noChildren = new HashSet<>(0);
public Set<EclipseContext> getChildren() {
Set<EclipseContext> result;
synchronized (children) {
if (children.isEmpty())
return noChildren;
result = new HashSet<>(children.size());
for (Iterator<WeakReference<EclipseContext>> i = children.iterator(); i.hasNext();) {
EclipseContext referredContext = i.next().get();
if (referredContext == null) {
i.remove();
continue;
}
result.add(referredContext);
}
}
return result;
}
@Override
public boolean containsKey(String name) {
trackAccess(name);
return containsKey(name, false);
}
public boolean containsKey(String name, boolean localOnly) {
if (isSetLocally(name))
return true;
if (localOnly)
return false;
EclipseContext parent = getParent();
if (parent != null && parent.containsKey(name, localOnly))
return true;
return false;
}
@Override
public void dispose() {
// dispose of child contexts first
for (EclipseContext childContext : getChildren()) {
childContext.dispose();
}
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.DISPOSE, null, null, null);
Set<Computation> allComputations = new HashSet<>();
allComputations.addAll(activeComputations.values());
allComputations.addAll(activeRATs);
activeComputations.clear();
activeRATs.clear();
Set<Scheduled> scheduled = new LinkedHashSet<>();
allComputations.addAll(getListeners());
weakListeners.clear();
for (Computation computation : allComputations) {
computation.handleInvalid(event, scheduled);
}
processScheduled(scheduled);
synchronized (notifyOnDisposal) {
for (IContextDisposalListener listener : notifyOnDisposal) {
listener.disposed(this);
}
notifyOnDisposal.clear();
}
for (ValueComputation computation : localValueComputations.values()) {
computation.dipose();
}
localValueComputations.clear();
// if this was the parent's active child, deactivate it
EclipseContext parent = getParent();
EclipseContext rootContext = null;
if (parent != null) {
rootContext = getRoot();
if (this == parent.getActiveChild())
parent.set(ACTIVE_CHILD, null);
}
localValues.clear();
if (parent != null) {
parent.removeChild(this);
if (rootContext != null) {
rootContext.cleanup();
}
// inform the OSGi layer via EventAdmin about the context disposal
// used for example to be able to cleanup cached requestors in
// ExtendedObjectSupplier implementations
EventAdmin admin = parent.get(EventAdmin.class);
if (admin != null) {
Event osgiEvent = new Event(IEclipseContext.TOPIC_DISPOSE, (Map<String, ?>) null);
admin.postEvent(osgiEvent);
}
}
if (debugAddOn != null) {
debugAddOn.notify(this, IEclipseContextDebugger.EventType.DISPOSED, null);
}
}
@Override
public Object get(String name) {
trackAccess(name);
return internalGet(this, name, false);
}
@Override
public Object getLocal(String name) {
trackAccess(name);
return internalGet(this, name, true);
}
public Object internalGet(EclipseContext originatingContext, String name, boolean local) {
if (this == originatingContext) {
ValueComputation valueComputation = localValueComputations.get(name);
if (valueComputation != null) {
Object result = valueComputation.get();
if (result != IInjector.NOT_A_VALUE) {
return result;
}
}
}
Object result = null;
// 1. try for local value
if (localValues.containsKey(name)) {
result = localValues.get(name);
if (result == null)
return null;
} else
result = lookup(name, originatingContext);
// if we found something, compute the concrete value and return
if (result != null) {
if (result instanceof IContextFunction) {
ValueComputation valueComputation = new ValueComputation(name, originatingContext, ((IContextFunction) result));
// do calculations before adding listeners
result = valueComputation.get();
originatingContext.localValueComputations.put(name, valueComputation);
}
if (result != IInjector.NOT_A_VALUE) {
return result;
}
}
// 3. delegate to parent
if (!local) {
IEclipseContext parent = (IEclipseContext) localValues.get(PARENT);
if (parent != null) {
return ((EclipseContext) parent).internalGet(originatingContext, name, local);
}
}
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, Object newValue, Set<Scheduled> scheduled) {
ContextChangeEvent event = null;
ValueComputation computation = localValueComputations.get(name);
if (computation != null) {
event = new ContextChangeEvent(this, eventType, null, name, oldValue);
if (computation.shouldRemove(event)) {
localValueComputations.remove(name);
weakListeners.remove(computation);
}
computation.handleInvalid(event, scheduled);
}
Set<Computation> namedComputations = weakListeners.getListeners(name);
if (namedComputations != null && !namedComputations.isEmpty()) {
if (event == null) {
event = new ContextChangeEvent(this, eventType, null, name, oldValue);
}
for (Computation listener : namedComputations) {
listener.handleInvalid(event, scheduled);
}
}
// invalidate this name in child contexts
for (EclipseContext childContext : getChildren()) {
// unless it is already set in this context (and thus hides the change)
if ((eventType == ContextChangeEvent.ADDED || eventType == ContextChangeEvent.REMOVED) && childContext.isSetLocally(name))
continue;
childContext.invalidate(name, eventType, oldValue, newValue, scheduled);
}
}
protected boolean isLocalEquals(String name, Object newValue) {
if (!localValues.containsKey(name))
return false;
return (localValues.get(name) == newValue);
}
private boolean isSetLocally(String name) {
return localValues.containsKey(name);
}
@Override
public void remove(String name) {
if (isSetLocally(name)) {
Object oldValue = localValues.remove(name);
Set<Scheduled> scheduled = new LinkedHashSet<>();
invalidate(name, ContextChangeEvent.REMOVED, oldValue, IInjector.NOT_A_VALUE, scheduled);
processScheduled(scheduled);
}
}
@Override
public void runAndTrack(final RunAndTrack runnable) {
TrackableComputationExt computation = new TrackableComputationExt(runnable, this);
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.INITIAL, null, null, null);
boolean result = computation.update(event);
if (result) {
Reference<Object> ref = computation.getReference();
if (ref != null)
activeComputations.put(ref, computation);
else
activeRATs.add(computation);
}
}
public void removeRAT(Computation computation) {
// remove from listeners
weakListeners.remove(computation);
activeRATs.remove(computation);
}
protected void processScheduled(Set<Scheduled> scheduledList) {
for (Scheduled scheduled : scheduledList) {
scheduled.runnable.update(scheduled.event);
}
}
@Override
public void set(String name, Object value) {
if (PARENT.equals(name)) {
setParent((IEclipseContext) value);
return;
}
boolean containsKey = localValues.containsKey(name);
Object oldValue = localValues.put(name, value);
if (!containsKey || oldValue != value) {
Set<Scheduled> scheduled = new LinkedHashSet<>();
invalidate(name, ContextChangeEvent.ADDED, oldValue, value, scheduled);
processScheduled(scheduled);
}
// cleanup unused computation listeners
Reference<?> ref = referenceQueue.poll();
if (ref != null) {
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.UNINJECTED, nullArgs, null, null);
for (; ref != null; ref = referenceQueue.poll()) {
TrackableComputationExt obsoleteComputation = activeComputations.remove(ref);
if (obsoleteComputation == null)
continue;
obsoleteComputation.update(event);
}
}
}
@Override
public void modify(String name, Object value) {
Set<Scheduled> scheduled = new LinkedHashSet<>();
if (!internalModify(name, value, scheduled))
set(name, value);
processScheduled(scheduled);
}
public boolean internalModify(String name, Object value, Set<Scheduled> scheduled) {
boolean containsKey = localValues.containsKey(name);
if (containsKey) {
if (!checkModifiable(name)) {
String tmp = "Variable " + name + " is not modifiable in the context " + this; //$NON-NLS-1$ //$NON-NLS-2$
throw new IllegalArgumentException(tmp);
}
Object oldValue = localValues.put(name, value);
if (oldValue != value)
invalidate(name, ContextChangeEvent.ADDED, oldValue, value, scheduled);
return true;
}
EclipseContext parent = getParent();
if (parent != null)
return parent.internalModify(name, value, scheduled);
return false;
}
@Override
public EclipseContext getParent() {
return (EclipseContext) localValues.get(PARENT);
}
@Override
public void setParent(IEclipseContext parent) {
EclipseContext parentContext = (EclipseContext) localValues.get(PARENT);
if (parent == parentContext)
return; // no-op
if (parentContext != null)
parentContext.removeChild(this);
Set<Scheduled> scheduled = new LinkedHashSet<>();
handleReparent((EclipseContext) parent, scheduled);
localValues.put(PARENT, parent);
if (parent != null)
((EclipseContext) parent).addChild(this);
processScheduled(scheduled);
return;
}
/**
* Returns a string representation of this context for debugging purposes only.
*/
@Override
public String toString() {
Object debugString = localValues.get(DEBUG_STRING);
return debugString instanceof String ? ((String) debugString) : ANONYMOUS_CONTEXT_NAME;
}
private void trackAccess(String name) {
Stack<Computation> current = getCalculatedComputations();
if (current.isEmpty())
return;
Computation computation = current.peek(); // only track in the top-most one
if (computation == null)
return;
addDependency(name, computation);
}
public void addDependency(String name, Computation computation) {
weakListeners.add(name, computation);
}
@Override
public void declareModifiable(String name) {
if (name == null)
return;
if (modifiable == null)
modifiable = new HashSet<>(3);
modifiable.add(name);
if (localValues.containsKey(name))
return;
localValues.put(name, null);
}
private boolean checkModifiable(String name) {
if (modifiable == null)
return false;
return modifiable.contains(name);
}
public void removeListenersTo(Object object) {
if (object == null)
return;
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.UNINJECTED, new Object[] {object}, null, null);
Set<Computation> comps = getListeners();
for (Computation computation : comps) {
if (computation instanceof TrackableComputationExt)
((TrackableComputationExt) computation).update(event);
}
}
public Set<Computation> getListeners() {
return weakListeners.getListeners();
}
private void handleReparent(EclipseContext newParent, Set<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
Set<String> usedNames = new HashSet<>();
collectDependentNames(usedNames);
// 2) for each used name:
for (String name : usedNames) {
if (localValues.containsKey(name))
continue; // it is a local value
Object oldValue = get(name);
Object newValue = (newParent != null) ? newParent.internalGet(this, name, false) : null;
if (oldValue != newValue)
invalidate(name, ContextChangeEvent.ADDED, oldValue, newValue, scheduled);
}
invalidateLocalComputations(scheduled);
}
protected void invalidateLocalComputations(Set<Scheduled> scheduled) {
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.ADDED, null, null, null);
for (Computation computation : localValueComputations.values()) {
weakListeners.remove(computation);
computation.handleInvalid(event, scheduled);
}
localValueComputations.clear();
// We need to cleanup computations recursively see bug 468048
for (EclipseContext c : getChildren()) {
c.invalidateLocalComputations(scheduled);
}
}
private void collectDependentNames(Set<String> usedNames) {
Set<String> names = weakListeners.getNames();
usedNames.addAll(names);
for (EclipseContext childContext : getChildren()) {
childContext.collectDependentNames(usedNames);
}
}
@Override
public void processWaiting() {
// traverse to the root node
EclipseContext parent = getParent();
if (parent != null) {
parent.processWaiting();
return;
}
if (waiting == null || waiting.isEmpty())
return;
// create update notifications
Computation[] ls = waiting.toArray(new Computation[waiting.size()]);
waiting.clear();
ContextChangeEvent event = new ContextChangeEvent(this, ContextChangeEvent.UPDATE, null, null, null);
for (Computation element : ls) {
if (element instanceof TrackableComputationExt)
((TrackableComputationExt) element).update(event);
}
}
public void addWaiting(Computation cp) {
// traverse to the root node
EclipseContext parent = getParent();
if (parent != null) {
parent.addWaiting(cp);
return;
}
if (waiting == null) // could happen on re-parent
waiting = Collections.synchronizedList(new ArrayList<>());
waiting.add(cp);
}
protected EclipseContext getRoot() {
EclipseContext current = this;
EclipseContext root;
do {
root = current;
current = current.getParent();
} while (current != null);
return root;
}
public void addChild(EclipseContext childContext) {
synchronized (children) {
children.add(new WeakReference<>(childContext));
}
}
public void removeChild(EclipseContext childContext) {
synchronized (children) {
for (Iterator<WeakReference<EclipseContext>> i = children.iterator(); i.hasNext();) {
EclipseContext referredContext = i.next().get();
if (referredContext == null) {
i.remove();
continue;
}
if (referredContext == childContext) {
i.remove();
return;
}
}
}
}
@Override
public <T> T get(Class<T> clazz) {
return clazz.cast(get(clazz.getName()));
}
@Override
public boolean containsKey(Class<?> clazz) {
return containsKey(clazz.getName());
}
@Override
public <T> void set(Class<T> clazz, T value) {
set(clazz.getName(), value);
}
@Override
public void remove(Class<?> clazz) {
remove(clazz.getName());
}
@Override
public <T> T getLocal(Class<T> clazz) {
return clazz.cast(getLocal(clazz.getName()));
}
@Override
public <T> void modify(Class<T> clazz, T value) {
modify(clazz.getName(), value);
}
@Override
public void declareModifiable(Class<?> clazz) {
declareModifiable(clazz.getName());
}
@Override
public IEclipseContext createChild() {
return new EclipseContext(this); // strategies are not inherited
}
@Override
public IEclipseContext createChild(String name) {
IEclipseContext result = createChild();
result.set(DEBUG_STRING, name);
return result;
}
public void notifyOnDisposal(IContextDisposalListener listener) {
synchronized (notifyOnDisposal) {
notifyOnDisposal.add(listener);
}
}
@Override
public IEclipseContext getActiveChild() {
trackAccess(ACTIVE_CHILD);
return (EclipseContext) internalGet(this, ACTIVE_CHILD, true);
}
@Override
public IEclipseContext getActiveLeaf() {
IEclipseContext activeContext = this;
IEclipseContext child = getActiveChild();
while (child != null) {
activeContext = child;
child = child.getActiveChild();
}
return activeContext;
}
@Override
public void activate() {
EclipseContext parent = getParent();
if (parent == null)
return;
if (this == parent.getActiveChild())
return;
parent.set(ACTIVE_CHILD, this);
if (debugAddOn != null) {
debugAddOn.notify(this, IEclipseContextDebugger.EventType.ACTIVATED, null);
}
}
@Override
public void activateBranch() {
for (IEclipseContext i = this; i != null; i = i.getParent()) {
i.activate();
}
}
@Override
public void deactivate() {
EclipseContext parent = getParent();
if (parent == null)
return;
if (this != parent.getActiveChild())
return; // this is not an active context; return
parent.set(ACTIVE_CHILD, null);
if (debugAddOn != null) {
debugAddOn.notify(this, IEclipseContextDebugger.EventType.DEACTIVATED, null);
}
}
// This method is for debug only, do not use externally
public Map<String, Object> localData() {
Map<String, Object> result = new HashMap<>(localValues.size());
for (Map.Entry<String, Object> entry : localValues.entrySet()) {
if (entry.getValue() instanceof IContextFunction) {
continue;
}
result.put(entry.getKey(), entry.getValue());
}
return result;
}
// This method is for debug only, do not use externally
public Map<String, Object> localContextFunction() {
Map<String, Object> result = new HashMap<>(localValues.size());
for (Map.Entry<String, Object> entry : localValues.entrySet()) {
if (entry.getValue() instanceof IContextFunction) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
// This method is for debug only, do not use externally
public Map<String, Object> cachedCachedContextFunctions() {
Map<String, Object> result = new HashMap<>(localValueComputations.size());
for (Map.Entry<String, ValueComputation> entry : localValueComputations.entrySet()) {
if (entry.getValue() != null) {
Object r = entry.getValue();
if (r != IInjector.NOT_A_VALUE) {
result.put(entry.getKey(), entry.getValue());
}
}
}
return result;
}
// This method is for debug only, do not use externally
public Set<String> getRawListenerNames() {
return weakListeners.getNames();
}
// This method is for debug only, do not use externally
public Set<Computation> getListeners(String name) {
return weakListeners.getListeners(name);
}
static public Stack<Computation> getCalculatedComputations() {
Stack<Computation> current = currentComputation.get();
if (current == null) {
current = new Stack<>();
currentComputation.set(current);
}
return current;
}
public void pushComputation(Computation comp) {
Stack<Computation> current = getCalculatedComputations();
current.push(comp);
}
public boolean hasComputation(Computation comp) {
Stack<Computation> current = getCalculatedComputations();
boolean hasComputation = current.contains(comp);
return hasComputation;
}
public void popComputation(Computation comp) {
Stack<Computation> current = getCalculatedComputations();
Computation ended = current.pop();
if (ended != comp)
throw new IllegalArgumentException("Internal error: Invalid nested computation processing"); //$NON-NLS-1$
}
/**
* This method can be overriden to provide additional source for the requested data.
* The override is expected to take care of initiating dynamic updates.
*/
protected Object lookup(String name, EclipseContext originatingContext) {
return null;
}
@Override
public <T> T getActive(Class<T> clazz) {
return clazz.cast(getActive(clazz.getName()));
}
@Override
public Object getActive(final String name) {
return getActiveLeaf().get(name);
}
public WeakReference<Object> trackedWeakReference(Object object) {
return new WeakReference<>(object, referenceQueue);
}
public void cleanup() {
for (EclipseContext childContext : getChildren()) {
childContext.cleanup();
}
weakListeners.cleanup();
}
}