| /******************************************************************************* |
| * 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; |
| } |
| |
| } |