org.eclipse.incquery.runtime.base.core.NavigationHelperImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.incquery.runtime.base.core.NavigationHelperImpl.java

Source

/*******************************************************************************
 * Copyright (c) 2010-2012, Istvan Rath and Daniel Varro
 * 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:
 *   Tamas Szabo, Gabor Bergmann - initial API and implementation
 *******************************************************************************/
package org.eclipse.incquery.runtime.base.core;

import static com.google.common.base.Preconditions.checkArgument;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;

import org.apache.log4j.Logger;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.notify.NotifyingList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.incquery.runtime.base.api.BaseIndexOptions;
import org.eclipse.incquery.runtime.base.api.DataTypeListener;
import org.eclipse.incquery.runtime.base.api.EMFBaseIndexChangeListener;
import org.eclipse.incquery.runtime.base.api.FeatureListener;
import org.eclipse.incquery.runtime.base.api.IEClassifierProcessor.IEClassProcessor;
import org.eclipse.incquery.runtime.base.api.IEClassifierProcessor.IEDataTypeProcessor;
import org.eclipse.incquery.runtime.base.api.IEMFIndexingErrorListener;
import org.eclipse.incquery.runtime.base.api.IEStructuralFeatureProcessor;
import org.eclipse.incquery.runtime.base.api.InstanceListener;
import org.eclipse.incquery.runtime.base.api.LightweightEObjectObserver;
import org.eclipse.incquery.runtime.base.api.NavigationHelper;
import org.eclipse.incquery.runtime.base.api.filters.IBaseIndexObjectFilter;
import org.eclipse.incquery.runtime.base.api.filters.IBaseIndexResourceFilter;
import org.eclipse.incquery.runtime.base.comprehension.EMFModelComprehension;
import org.eclipse.incquery.runtime.base.comprehension.EMFVisitor;
import org.eclipse.incquery.runtime.base.exception.IncQueryBaseException;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;

public class NavigationHelperImpl implements NavigationHelper {

    protected boolean inWildcardMode;

    protected Notifier notifier;
    protected Set<Notifier> modelRoots;
    private boolean expansionAllowed;
    // protected NavigationHelperVisitor visitor;
    protected NavigationHelperContentAdapter contentAdapter;

    private final Logger logger;

    // type object or String id
    protected Set<Object> directlyObservedClasses = new HashSet<Object>();
    // including subclasses; if null, must be recomputed
    protected Set<Object> allObservedClasses = null;
    protected Set<Object> observedDataTypes;
    protected Set<Object> observedFeatures;
    // ignore RESOLVE for these features, as they are just starting to be observed - see [428458]
    protected Set<Object> ignoreResolveNotificationFeatures;

    /**
     * Feature registration and model traversal is delayed while true
     */
    protected boolean delayTraversals = false;
    /**
     * Classes (or String ID in dynamic mode) to be registered once the coalescing period is over
     */
    protected Set<Object> delayedClasses;
    /**
     * EStructuralFeatures (or String ID in dynamic mode) to be registered once the coalescing period is over
     */
    protected Set<Object> delayedFeatures;
    /**
     * EDataTypes (or String ID in dynamic mode) to be registered once the coalescing period is over
     */
    protected Set<Object> delayedDataTypes;

    /**
     * Features per EObject to be resolved later (towards the end of a coalescing period when no Resources are loading)
     */
    protected Multimap<EObject, EReference> delayedProxyResolutions = LinkedHashMultimap.create();
    /**
     * Reasources that are currently loading, implying the proxy resolution attempts should be delayed
     */
    protected Set<Resource> resolutionDelayingResources = new HashSet<Resource>();

    /**
     * These global listeners will be called after updates.
     */
    //private final Set<Runnable> afterUpdateCallbacks;
    private final Set<EMFBaseIndexChangeListener> baseIndexChangeListeners;
    private final Map<LightweightEObjectObserver, Collection<EObject>> lightweightObservers;

    // These are the user subscriptions to notifications
    private final Map<InstanceListener, Set<EClass>> subscribedInstanceListeners;
    private final Map<FeatureListener, Set<EStructuralFeature>> subscribedFeatureListeners;
    private final Map<DataTypeListener, Set<EDataType>> subscribedDataTypeListeners;

    // these are the internal notification tables
    // (element Type or String id) -> listener -> (subscription types)
    // if null, must be recomputed from subscriptions
    // potentially multiple subscription types for each element type because (a) nsURI collisions, (b) multiple supertypes
    private Table<Object, InstanceListener, Set<EClass>> instanceListeners;
    private Table<Object, FeatureListener, Set<EStructuralFeature>> featureListeners;
    private Table<Object, DataTypeListener, Set<EDataType>> dataTypeListeners;

    private final Set<IEMFIndexingErrorListener> errorListeners;
    private final BaseIndexOptions baseIndexOptions;

    private EMFModelComprehension comprehension;

    <T> Set<T> setMinus(Collection<? extends T> a, Collection<T> b) {
        Set<T> result = new HashSet<T>(a);
        result.removeAll(b);
        return result;
    }

    @SuppressWarnings("unchecked")
    <T extends EObject> Set<T> resolveAllInternal(Set<? extends T> a) {
        if (a == null)
            a = Collections.emptySet();
        Set<T> result = new HashSet<T>();
        for (T t : a) {
            if (t.eIsProxy()) {
                result.add((T) EcoreUtil.resolve(t, (ResourceSet) null));
            } else {
                result.add(t);
            }
        }
        return result;
    }

    Set<Object> resolveClassifiersToKey(Set<? extends EClassifier> classes) {
        Set<? extends EClassifier> resolveds = resolveAllInternal(classes);
        Set<Object> result = new HashSet<Object>();
        for (EClassifier resolved : resolveds) {
            result.add(toKey(resolved));
        }
        return result;
    }

    Set<Object> resolveFeaturesToKey(Set<? extends EStructuralFeature> features) {
        Set<EStructuralFeature> resolveds = resolveAllInternal(features);
        Set<Object> result = new HashSet<Object>();
        for (EStructuralFeature resolved : resolveds) {
            result.add(toKey(resolved));
        }
        return result;
    }

    @Override
    public boolean isInWildcardMode() {
        return baseIndexOptions.isWildcardMode();
    }

    @Override
    public boolean isInDynamicEMFMode() {
        return baseIndexOptions.isDynamicEMFMode();
    }

    /**
     * @return the baseIndexOptions
     */
    public BaseIndexOptions getBaseIndexOptions() {
        return baseIndexOptions.copy();
    }

    /**
     * @return the comprehension
     */
    public EMFModelComprehension getComprehension() {
        return comprehension;
    }

    public NavigationHelperImpl(Notifier emfRoot, BaseIndexOptions options, Logger logger)
            throws IncQueryBaseException {
        this.baseIndexOptions = options.copy();
        this.logger = logger;
        assert (logger != null);

        this.comprehension = new EMFModelComprehension(baseIndexOptions);
        this.subscribedInstanceListeners = new HashMap<InstanceListener, Set<EClass>>();
        this.subscribedFeatureListeners = new HashMap<FeatureListener, Set<EStructuralFeature>>();
        this.subscribedDataTypeListeners = new HashMap<DataTypeListener, Set<EDataType>>();
        this.lightweightObservers = new HashMap<LightweightEObjectObserver, Collection<EObject>>();
        this.observedFeatures = new HashSet<Object>();
        this.ignoreResolveNotificationFeatures = new HashSet<Object>();
        this.observedDataTypes = new HashSet<Object>();
        this.contentAdapter = new NavigationHelperContentAdapter(this);
        this.baseIndexChangeListeners = new HashSet<EMFBaseIndexChangeListener>();
        this.errorListeners = new LinkedHashSet<IEMFIndexingErrorListener>();

        this.notifier = emfRoot;
        this.modelRoots = new HashSet<Notifier>();
        this.expansionAllowed = false;

        if (emfRoot != null) {
            addRootInternal(emfRoot);
        }
    }

    public NavigationHelperContentAdapter getContentAdapter() {
        return contentAdapter;
    }

    public Set<Object> getObservedFeaturesInternal() {
        return observedFeatures;
    }

    public boolean isFeatureResolveIgnored(EStructuralFeature feature) {
        return ignoreResolveNotificationFeatures.contains(toKey(feature));
    }

    @Override
    public void dispose() {
        ensureNoListenersForDispose();
        for (Notifier root : modelRoots) {
            contentAdapter.removeAdapter(root);
        }
    }

    @Override
    public Set<Object> getDataTypeInstances(EDataType type) {
        Object typeKey = toKey(type);
        Map<Object, Integer> valMap = contentAdapter.getDataTypeMap(typeKey);
        if (valMap != null) {
            return Collections.unmodifiableSet(valMap.keySet());
        } else {
            return Collections.emptySet();
        }
    }

    @Override
    public Set<Setting> findByAttributeValue(Object value_) {
        Object value = toCanonicalValueRepresentation(value_);
        Set<Setting> retSet = new HashSet<Setting>();
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(value);

        for (Entry<Object, Collection<EObject>> entry : valMap.entrySet()) {
            final Collection<EObject> holders = entry.getValue();
            EStructuralFeature feature = contentAdapter.getKnownFeatureForKey(entry.getKey());
            for (EObject holder : NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders)) {
                retSet.add(new NavigationHelperSetting(feature, holder, value));
            }
        }

        return retSet;
    }

    @Override
    public Set<Setting> findByAttributeValue(Object value_, Collection<EAttribute> attributes) {
        Object value = toCanonicalValueRepresentation(value_);
        Set<Setting> retSet = new HashSet<Setting>();
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(value);

        for (EAttribute attr : attributes) {
            Object feature = toKey(attr);
            final Collection<EObject> holders = valMap.get(feature);
            if (holders != null) {
                for (EObject holder : NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders)) {
                    retSet.add(new NavigationHelperSetting(attr, holder, value));
                }
            }
        }

        return retSet;
    }

    @Override
    public Set<EObject> findByAttributeValue(Object value_, EAttribute attribute) {
        Object value = toCanonicalValueRepresentation(value_);
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(value);
        Object feature = toKey(attribute);
        final Collection<EObject> holders = valMap.get(feature);
        if (holders == null) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders));
        }
    }

    @Override
    public void processAllFeatureInstances(EStructuralFeature feature, IEStructuralFeatureProcessor processor) {
        final Map<Object, Collection<EObject>> instanceMap = contentAdapter.getValueToFeatureToHolderMap()
                .column(toKey(feature));
        for (Entry<Object, Collection<EObject>> entry : instanceMap.entrySet()) {
            final Collection<EObject> holders = entry.getValue();
            for (EObject src : NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders)) {
                processor.process(feature, src, entry.getKey());
            }
        }
    }

    @Override
    public void processDirectInstances(EClass type, IEClassProcessor processor) {
        Object typeKey = toKey(type);
        processDirectInstancesInternal(type, processor, typeKey);
    }

    @Override
    public void processAllInstances(EClass type, IEClassProcessor processor) {
        Object typeKey = toKey(type);
        Set<Object> subTypes = contentAdapter.getSubTypeMap().get(typeKey);
        if (subTypes != null) {
            for (Object subTypeKey : subTypes) {
                processDirectInstancesInternal(type, processor, subTypeKey);
            }
        }
        processDirectInstancesInternal(type, processor, typeKey);
    }

    @Override
    public void processDataTypeInstances(EDataType type, IEDataTypeProcessor processor) {
        Object typeKey = toKey(type);
        Map<Object, Integer> valMap = contentAdapter.getDataTypeMap(typeKey);
        if (valMap == null) {
            return;
        }
        for (Object value : valMap.keySet()) {
            processor.process(type, value);
        }
    }

    private void processDirectInstancesInternal(EClass type, IEClassProcessor processor, Object typeKey) {
        final Set<EObject> instances = contentAdapter.getInstanceSet(typeKey);
        if (instances != null) {
            for (EObject eObject : instances) {
                processor.process(type, eObject);
            }
        }
    }

    @Override
    public Set<Setting> getInverseReferences(EObject target) {
        Set<Setting> retSet = new HashSet<Setting>();
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(target);

        for (Entry<Object, Collection<EObject>> entry : valMap.entrySet()) {
            final Collection<EObject> holders = entry.getValue();
            for (EObject source : NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders)) {
                EStructuralFeature feature = contentAdapter.getKnownFeatureForKey(entry.getKey());
                retSet.add(new NavigationHelperSetting(feature, source, target));
            }
        }

        return retSet;
    }

    @Override
    public Set<Setting> getInverseReferences(EObject target, Collection<EReference> references) {
        Set<Setting> retSet = new HashSet<Setting>();
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(target);

        for (EReference ref : references) {
            Object feature = toKey(ref);
            final Collection<EObject> holders = valMap.get(feature);
            if (holders != null) {
                for (EObject source : NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders)) {
                    retSet.add(new NavigationHelperSetting(ref, source, target));
                }
            }
        }

        return retSet;
    }

    @Override
    public Set<EObject> getInverseReferences(EObject target, EReference reference) {
        Object feature = toKey(reference);
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(target);
        final Collection<EObject> holders = valMap.get(feature);
        if (holders == null) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders));
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<EObject> getReferenceValues(EObject source, EReference reference) {
        Set<Object> targets = getFeatureTargets(source, reference);
        return (Set<EObject>) (Set<?>) targets; // this is known to be safe, as EReferences can only point to EObjects
    }

    @Override
    public Set<Object> getFeatureTargets(EObject source, EStructuralFeature _feature) {
        Object feature = toKey(_feature);
        final Set<Object> valSet = contentAdapter.getHolderToFeatureToValueMap().get(source, feature);
        if (valSet == null) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(valSet);
        }
    }

    @Override
    public Map<EObject, Set<Object>> getFeatureInstances(EStructuralFeature _feature) {
        Object feature = toKey(_feature);
        final Map<EObject, Set<Object>> valMap = contentAdapter.getHolderToFeatureToValueMap().column(feature);
        if (valMap == null) {
            return Collections.emptyMap();
        } else {
            return Collections.unmodifiableMap(valMap);
        }
    }

    @Override
    public Set<EObject> getDirectInstances(EClass type) {
        Object typeKey = toKey(type);
        Set<EObject> valSet = contentAdapter.getInstanceSet(typeKey);
        if (valSet == null) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(valSet);
        }
    }

    private Object toKey(EClassifier eClassifier) {
        return contentAdapter.toKey(eClassifier);
    }

    private Object toKey(EStructuralFeature feature) {
        return contentAdapter.toKey(feature);
    }

    @Override
    public Object toCanonicalValueRepresentation(Object value) {
        return contentAdapter.toInternalValueRepresentation(value);
    }

    @Override
    public Set<EObject> getAllInstances(EClass type) {
        Set<EObject> retSet = new HashSet<EObject>();

        Object typeKey = toKey(type);
        Set<Object> subTypes = contentAdapter.getSubTypeMap().get(typeKey);
        if (subTypes != null) {
            for (Object subTypeKey : subTypes) {
                final Set<EObject> instances = contentAdapter.getInstanceSet(subTypeKey);
                if (instances != null) {
                    retSet.addAll(instances);
                }
            }
        }
        final Set<EObject> instances = contentAdapter.getInstanceSet(typeKey);
        if (instances != null) {
            retSet.addAll(instances);
        }

        return retSet;
    }

    @Override
    public Set<EObject> findByFeatureValue(Object value_, EStructuralFeature _feature) {
        Object value = toCanonicalValueRepresentation(value_);
        Object feature = toKey(_feature);
        Set<EObject> retSet = new HashSet<EObject>();
        Map<Object, Collection<EObject>> valMap = contentAdapter.getValueToFeatureToHolderMap().row(value);
        final Collection<EObject> holders = valMap.get(feature);
        if (holders != null) {
            retSet.addAll(NavigationHelperContentAdapter.holderCollectionToUniqueSet(holders));
        }
        return retSet;
    }

    @Override
    public Set<EObject> getHoldersOfFeature(EStructuralFeature _feature) {
        Object feature = toKey(_feature);
        Multiset<EObject> holders = contentAdapter.getFeatureToHolderMap().get(feature);
        if (holders == null) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableSet(holders.elementSet());
        }
    }

    @Override
    public void addInstanceListener(Collection<EClass> classes, InstanceListener listener) {
        Set<EClass> registered = this.subscribedInstanceListeners.get(listener);
        if (registered == null) {
            registered = new HashSet<EClass>();
            this.subscribedInstanceListeners.put(listener, registered);
        }
        Set<EClass> delta = setMinus(classes, registered);
        if (!delta.isEmpty()) {
            registered.addAll(delta);
            if (instanceListeners != null) { // if already computed
                for (EClass subscriptionType : delta) {
                    final Object superElementTypeKey = toKey(subscriptionType);
                    addInstanceListenerInternal(listener, subscriptionType, superElementTypeKey);
                    final Set<Object> subTypeKeys = contentAdapter.getSubTypeMap().get(superElementTypeKey);
                    if (subTypeKeys != null)
                        for (Object subTypeKey : subTypeKeys) {
                            addInstanceListenerInternal(listener, subscriptionType, subTypeKey);
                        }
                }
            }
        }
    }

    @Override
    public void removeInstanceListener(Collection<EClass> classes, InstanceListener listener) {
        Set<EClass> restriction = this.subscribedInstanceListeners.get(listener);
        if (restriction != null) {
            boolean changed = restriction.removeAll(classes);
            if (restriction.size() == 0) {
                this.subscribedInstanceListeners.remove(listener);
            }
            if (changed)
                instanceListeners = null; // recompute later on demand
        }
    }

    @Override
    public void addFeatureListener(Collection<? extends EStructuralFeature> features, FeatureListener listener) {
        Set<EStructuralFeature> registered = this.subscribedFeatureListeners.get(listener);
        if (registered == null) {
            registered = new HashSet<EStructuralFeature>();
            this.subscribedFeatureListeners.put(listener, registered);
        }
        Set<EStructuralFeature> delta = setMinus(features, registered);
        if (!delta.isEmpty()) {
            registered.addAll(delta);
            if (featureListeners != null) { // if already computed
                for (EStructuralFeature subscriptionType : delta) {
                    addFeatureListenerInternal(listener, subscriptionType, toKey(subscriptionType));
                }
            }
        }
    }

    @Override
    public void removeFeatureListener(Collection<? extends EStructuralFeature> features, FeatureListener listener) {
        Collection<EStructuralFeature> restriction = this.subscribedFeatureListeners.get(listener);
        if (restriction != null) {
            boolean changed = restriction.removeAll(features);
            if (restriction.size() == 0) {
                this.subscribedFeatureListeners.remove(listener);
            }
            if (changed)
                featureListeners = null; // recompute later on demand
        }
    }

    @Override
    public void addDataTypeListener(Collection<EDataType> types, DataTypeListener listener) {
        Set<EDataType> registered = this.subscribedDataTypeListeners.get(listener);
        if (registered == null) {
            registered = new HashSet<EDataType>();
            this.subscribedDataTypeListeners.put(listener, registered);
        }
        Set<EDataType> delta = setMinus(types, registered);
        if (!delta.isEmpty()) {
            registered.addAll(delta);
            if (dataTypeListeners != null) { // if already computed
                for (EDataType subscriptionType : delta) {
                    addDatatypeListenerInternal(listener, subscriptionType, toKey(subscriptionType));
                }
            }
        }
    }

    @Override
    public void removeDataTypeListener(Collection<EDataType> types, DataTypeListener listener) {
        Collection<EDataType> restriction = this.subscribedDataTypeListeners.get(listener);
        if (restriction != null) {
            boolean changed = restriction.removeAll(types);
            if (restriction.size() == 0) {
                this.subscribedDataTypeListeners.remove(listener);
            }
            if (changed)
                dataTypeListeners = null; // recompute later on demand
        }
    }

    /**
     * @return the observedDataTypes
     */
    public Set<Object> getObservedDataTypesInternal() {
        return observedDataTypes;
    }

    @Override
    public boolean addLightweightEObjectObserver(LightweightEObjectObserver observer, EObject observedObject) {
        Collection<EObject> observedObjects = lightweightObservers.get(observer);
        if (observedObjects == null) {
            observedObjects = new HashSet<EObject>();
            lightweightObservers.put(observer, observedObjects);
        }
        return observedObjects.add(observedObject);
    }

    @Override
    public boolean removeLightweightEObjectObserver(LightweightEObjectObserver observer, EObject observedObject) {
        boolean result = false;
        Collection<EObject> observedObjects = lightweightObservers.get(observer);
        if (observedObjects != null) {
            result = observedObjects.remove(observedObject);
            if (observedObjects.isEmpty()) {
                lightweightObservers.remove(observer);
            }
        }
        return result;
    }

    /**
     * @return the lightweightObservers
     */
    public Map<LightweightEObjectObserver, Collection<EObject>> getLightweightObservers() {
        return lightweightObservers;
    }

    /**
     * This will run after updates.
     */
    protected void notifyBaseIndexChangeListeners(boolean baseIndexChanged) {
        if (!baseIndexChangeListeners.isEmpty()) {
            for (EMFBaseIndexChangeListener listener : new ArrayList<EMFBaseIndexChangeListener>(
                    baseIndexChangeListeners)) {
                try {
                    if (!listener.onlyOnIndexChange() || baseIndexChanged) {
                        listener.notifyChanged(baseIndexChanged);
                    }
                } catch (Exception ex) {
                    notifyFatalListener(
                            "EMF-IncQuery Base encountered an error in delivering notifications about changes. ",
                            ex);
                }
            }
        }
    }

    @Override
    public void addBaseIndexChangeListener(EMFBaseIndexChangeListener listener) {
        checkArgument(listener != null, "Cannot add null listener!");
        baseIndexChangeListeners.add(listener);
    }

    @Override
    public void removeBaseIndexChangeListener(EMFBaseIndexChangeListener listener) {
        checkArgument(listener != null, "Cannot remove null listener!");
        baseIndexChangeListeners.remove(listener);
    }

    @Override
    public boolean addIndexingErrorListener(IEMFIndexingErrorListener listener) {
        return errorListeners.add(listener);
    }

    @Override
    public boolean removeIndexingErrorListener(IEMFIndexingErrorListener listener) {
        return errorListeners.remove(listener);
    }

    public void notifyErrorListener(String message, Throwable t) {
        logger.error(message, t);
        for (IEMFIndexingErrorListener listener : errorListeners) {
            listener.error(message, t);
        }
    }

    public void notifyFatalListener(String message, Throwable t) {
        logger.fatal(message, t);
        for (IEMFIndexingErrorListener listener : errorListeners) {
            listener.fatal(message, t);
        }
    }

    protected void considerForExpansion(EObject obj) {
        if (expansionAllowed) {
            Resource eResource = obj.eResource();
            if (eResource != null && eResource.getResourceSet() == null) {
                expandToAdditionalRoot(eResource);
            }
        }
    }

    protected void expandToAdditionalRoot(Notifier root) {
        if (modelRoots.contains(root))
            return;

        if (root instanceof ResourceSet) {
            expansionAllowed = true;
        } else if (root instanceof Resource) {
            IBaseIndexResourceFilter resourceFilter = baseIndexOptions.getResourceFilterConfiguration();
            if (resourceFilter != null && resourceFilter.isResourceFiltered((Resource) root))
                return;
        }
        final IBaseIndexObjectFilter objectFilter = baseIndexOptions.getObjectFilterConfiguration();
        if (objectFilter != null && objectFilter.isFiltered(root))
            return;

        // no veto by filters
        modelRoots.add(root);
        contentAdapter.addAdapter(root);
        contentAdapter.notifyBaseIndexChangeListeners();
    }

    /**
     * @return the expansionAllowed
     */
    public boolean isExpansionAllowed() {
        return expansionAllowed;
    }

    /**
     * @return the directlyObservedClasses
     */
    public Set<Object> getDirectlyObservedClassesInternal() {
        return directlyObservedClasses;
    }

    boolean isObservedInternal(Object clazzKey) {
        return inWildcardMode || getAllObservedClassesInternal().contains(clazzKey);
    }

    /**
     * not just the directly observed classes, but also their known subtypes
     */
    public Set<Object> getAllObservedClassesInternal() {
        if (allObservedClasses == null) {
            allObservedClasses = new HashSet<Object>();
            for (Object eClassKey : directlyObservedClasses) {
                allObservedClasses.add(eClassKey);
                final Set<Object> subTypes = contentAdapter.getSubTypeMap().get(eClassKey);
                if (subTypes != null) {
                    allObservedClasses.addAll(subTypes);
                }
            }
        }
        return allObservedClasses;
    }

    /**
    * @return the instanceListeners
    */
    Table<Object, InstanceListener, Set<EClass>> getInstanceListeners() {
        if (instanceListeners == null) {
            instanceListeners = HashBasedTable.create(100, 1);
            for (Entry<InstanceListener, Set<EClass>> subscription : subscribedInstanceListeners.entrySet()) {
                final InstanceListener listener = subscription.getKey();
                for (EClass subscriptionType : subscription.getValue()) {
                    final Object superElementTypeKey = toKey(subscriptionType);
                    addInstanceListenerInternal(listener, subscriptionType, superElementTypeKey);
                    final Set<Object> subTypeKeys = contentAdapter.getSubTypeMap().get(superElementTypeKey);
                    if (subTypeKeys != null)
                        for (Object subTypeKey : subTypeKeys) {
                            addInstanceListenerInternal(listener, subscriptionType, subTypeKey);
                        }
                }
            }
        }
        return instanceListeners;
    }

    Table<Object, InstanceListener, Set<EClass>> peekInstanceListeners() {
        return instanceListeners;
    }

    void addInstanceListenerInternal(final InstanceListener listener, EClass subscriptionType,
            final Object elementTypeKey) {
        Set<EClass> subscriptionTypes = instanceListeners.get(elementTypeKey, listener);
        if (subscriptionTypes == null) {
            subscriptionTypes = new HashSet<EClass>();
            instanceListeners.put(elementTypeKey, listener, subscriptionTypes);
        }
        subscriptionTypes.add(subscriptionType);
    }

    /**
     * @return the featureListeners
     */
    Table<Object, FeatureListener, Set<EStructuralFeature>> getFeatureListeners() {
        if (featureListeners == null) {
            featureListeners = HashBasedTable.create(100, 1);
            for (Entry<FeatureListener, Set<EStructuralFeature>> subscription : subscribedFeatureListeners
                    .entrySet()) {
                final FeatureListener listener = subscription.getKey();
                for (EStructuralFeature subscriptionType : subscription.getValue()) {
                    final Object elementTypeKey = toKey(subscriptionType);
                    addFeatureListenerInternal(listener, subscriptionType, elementTypeKey);
                }
            }
        }
        return featureListeners;
    }

    void addFeatureListenerInternal(final FeatureListener listener, EStructuralFeature subscriptionType,
            final Object elementTypeKey) {
        Set<EStructuralFeature> subscriptionTypes = featureListeners.get(elementTypeKey, listener);
        if (subscriptionTypes == null) {
            subscriptionTypes = new HashSet<EStructuralFeature>();
            featureListeners.put(elementTypeKey, listener, subscriptionTypes);
        }
        subscriptionTypes.add(subscriptionType);
    }

    /**
     * @return the dataTypeListeners
     */
    Table<Object, DataTypeListener, Set<EDataType>> getDataTypeListeners() {
        if (dataTypeListeners == null) {
            dataTypeListeners = HashBasedTable.create(100, 1);
            for (Entry<DataTypeListener, Set<EDataType>> subscription : subscribedDataTypeListeners.entrySet()) {
                final DataTypeListener listener = subscription.getKey();
                for (EDataType subscriptionType : subscription.getValue()) {
                    final Object elementTypeKey = toKey(subscriptionType);
                    addDatatypeListenerInternal(listener, subscriptionType, elementTypeKey);
                }
            }
        }
        return dataTypeListeners;
    }

    void addDatatypeListenerInternal(final DataTypeListener listener, EDataType subscriptionType,
            final Object elementTypeKey) {
        Set<EDataType> subscriptionTypes = dataTypeListeners.get(elementTypeKey, listener);
        if (subscriptionTypes == null) {
            subscriptionTypes = new HashSet<EDataType>();
            dataTypeListeners.put(elementTypeKey, listener, subscriptionTypes);
        }
        subscriptionTypes.add(subscriptionType);
    }

    @Override
    public void registerObservedTypes(Set<EClass> classes, Set<EDataType> dataTypes,
            Set<? extends EStructuralFeature> features) {
        ensureNotInWildcardMode();
        if (classes != null || features != null || dataTypes != null) {
            final Set<Object> resolvedFeatures = resolveFeaturesToKey(features);
            final Set<Object> resolvedClasses = resolveClassifiersToKey(classes);
            final Set<Object> resolvedDatatypes = resolveClassifiersToKey(dataTypes);

            try {
                coalesceTraversals(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        delayedFeatures.addAll(resolvedFeatures);
                        delayedDataTypes.addAll(resolvedDatatypes);
                        delayedClasses.addAll(resolvedClasses);
                        return null;
                    }
                });
            } catch (InvocationTargetException ex) {
                processingError(ex.getCause(), "register en masse the observed EClasses " + resolvedClasses
                        + " and EDatatypes " + resolvedDatatypes + " and EStructuralFeatures " + resolvedFeatures);
            } catch (Exception ex) {
                processingError(ex, "register en masse the observed EClasses " + resolvedClasses
                        + " and EDatatypes " + resolvedDatatypes + " and EStructuralFeatures " + resolvedFeatures);
            }
        }
    }

    @Override
    public void unregisterObservedTypes(Set<EClass> classes, Set<EDataType> dataTypes,
            Set<? extends EStructuralFeature> features) {
        unregisterEClasses(classes);
        unregisterEDataTypes(dataTypes);
        unregisterEStructuralFeatures(features);
    }

    @Override
    public void registerEStructuralFeatures(Set<? extends EStructuralFeature> features) {
        ensureNotInWildcardMode();
        if (features != null) {
            final Set<Object> resolved = resolveFeaturesToKey(features);

            try {
                coalesceTraversals(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        delayedFeatures.addAll(resolved);
                        return null;
                    }
                });
            } catch (InvocationTargetException ex) {
                processingError(ex.getCause(), "register the observed EStructuralFeatures: " + resolved);
            } catch (Exception ex) {
                processingError(ex, "register the observed EStructuralFeatures: " + resolved);
            }
        }
    }

    @Override
    public void unregisterEStructuralFeatures(Set<? extends EStructuralFeature> features) {
        ensureNotInWildcardMode();
        if (features != null) {
            final Set<Object> resolved = resolveFeaturesToKey(features);
            ensureNoListeners(resolved, getFeatureListeners());
            observedFeatures.removeAll(resolved);
            delayedFeatures.removeAll(resolved);
            for (Object f : resolved) {
                contentAdapter.getValueToFeatureToHolderMap().column(f).clear();
                if (contentAdapter.peekFeatureToHolderMap() != null) {
                    contentAdapter.peekFeatureToHolderMap().remove(f);
                }
                if (contentAdapter.peekHolderToFeatureToValueMap() != null) {
                    contentAdapter.peekHolderToFeatureToValueMap().column(f).clear();
                }
            }
        }
    }

    @Override
    public void registerEClasses(Set<EClass> classes) {
        ensureNotInWildcardMode();
        if (classes != null) {
            final Set<Object> resolvedClasses = resolveClassifiersToKey(classes);

            try {
                coalesceTraversals(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        delayedClasses.addAll(resolvedClasses);
                        return null;
                    }
                });
            } catch (InvocationTargetException ex) {
                processingError(ex.getCause(), "register the observed EClasses: " + resolvedClasses);
            } catch (Exception ex) {
                processingError(ex, "register the observed EClasses: " + resolvedClasses);
            }
        }
    }

    /**
     * @param classes
     */
    protected void startObservingClasses(Set<Object> classKeys) {
        directlyObservedClasses.addAll(classKeys);
        getAllObservedClassesInternal().addAll(classKeys);
        for (Object classKey : classKeys) {
            final Set<Object> subTypes = contentAdapter.getSubTypeMap().get(classKey);
            if (subTypes != null) {
                allObservedClasses.addAll(subTypes);
            }
        }
    }

    @Override
    public void unregisterEClasses(Set<EClass> classes) {
        ensureNotInWildcardMode();
        if (classes != null) {
            final Set<Object> resolved = resolveClassifiersToKey(classes);
            ensureNoListeners(resolved, getInstanceListeners());
            directlyObservedClasses.removeAll(resolved);
            allObservedClasses = null;
            delayedClasses.removeAll(resolved);
            for (Object c : resolved) {
                contentAdapter.removeInstanceSet(c);
            }
        }
    }

    @Override
    public void registerEDataTypes(Set<EDataType> dataTypes) {
        ensureNotInWildcardMode();
        if (dataTypes != null) {
            final Set<Object> resolved = resolveClassifiersToKey(dataTypes);

            try {
                coalesceTraversals(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        delayedDataTypes.addAll(resolved);
                        return null;
                    }
                });
            } catch (InvocationTargetException ex) {
                processingError(ex.getCause(), "register the observed EDataTypes: " + resolved);
            } catch (Exception ex) {
                processingError(ex, "register the observed EDataTypes: " + resolved);
            }
        }
    }

    @Override
    public void unregisterEDataTypes(Set<EDataType> dataTypes) {
        ensureNotInWildcardMode();
        if (dataTypes != null) {
            final Set<Object> resolved = resolveClassifiersToKey(dataTypes);
            ensureNoListeners(resolved, getDataTypeListeners());
            observedDataTypes.removeAll(resolved);
            delayedDataTypes.removeAll(resolved);
            for (Object dataType : resolved) {
                contentAdapter.removeDataTypeMap(dataType);
            }
        }
    }

    @Override
    public boolean isCoalescing() {
        return delayTraversals;
    }

    @Override
    public <V> V coalesceTraversals(Callable<V> callable) throws InvocationTargetException {
        V finalResult = null;

        if (delayTraversals) { // reentrant case, no special action needed
            try {
                finalResult = callable.call();
            } catch (Exception e) {
                throw new InvocationTargetException(e);
            }
            return finalResult;
        }

        boolean firstRun = true;
        while (callable != null) { // repeat if post-processing needed     
            delayedClasses = new HashSet<Object>();
            delayedFeatures = new HashSet<Object>();
            delayedDataTypes = new HashSet<Object>();

            try {
                try {
                    delayTraversals = true;

                    V result = callable.call();
                    if (firstRun) {
                        firstRun = false;
                        finalResult = result;
                    }

                    // are there proxies left to be resolved? are we allowed to resolve them now?
                    while ((!delayedProxyResolutions.isEmpty()) && resolutionDelayingResources.isEmpty()) {
                        // pop first entry
                        final Collection<Entry<EObject, EReference>> entries = delayedProxyResolutions.entries();
                        final Entry<EObject, EReference> toResolve = entries.iterator().next();
                        entries.remove(toResolve);

                        // see if we can resolve proxies
                        comprehension.tryResolveReference(toResolve.getKey(), toResolve.getValue());
                    }

                } finally {
                    delayTraversals = false;
                    callable = null;

                    delayedFeatures = setMinus(delayedFeatures, observedFeatures);
                    delayedClasses = setMinus(delayedClasses, directlyObservedClasses);
                    delayedDataTypes = setMinus(delayedDataTypes, observedDataTypes);

                    boolean classesWarrantTraversal = !setMinus(delayedClasses, getAllObservedClassesInternal())
                            .isEmpty();

                    if (!delayedClasses.isEmpty() || !delayedFeatures.isEmpty() || !delayedDataTypes.isEmpty()) {
                        final Set<Object> oldClasses = new HashSet<Object>(directlyObservedClasses);
                        startObservingClasses(delayedClasses);
                        observedDataTypes.addAll(delayedDataTypes);
                        observedFeatures.addAll(delayedFeatures);

                        // make copies so that original accumulators can be cleaned for the next cycle
                        // or for the rare case that a coalesced  traversal is invoked during visitation, 
                        // e.g. by a derived feature implementation
                        final Set<Object> toGatherClasses = new HashSet<Object>(delayedClasses);
                        final Set<Object> toGatherFeatures = new HashSet<Object>(delayedFeatures);
                        final Set<Object> toGatherDataTypes = new HashSet<Object>(delayedDataTypes);

                        if (classesWarrantTraversal || !toGatherFeatures.isEmpty()
                                || !toGatherDataTypes.isEmpty()) {
                            // repeat the cycle with this visit
                            final NavigationHelperVisitor visitor = new NavigationHelperVisitor.TraversingVisitor(
                                    this, toGatherFeatures, toGatherClasses, oldClasses, toGatherDataTypes);

                            callable = new Callable<V>() {
                                @Override
                                public V call() throws Exception {
                                    // temporarily ignoring RESOLVE on these features, as they were not observed before
                                    ignoreResolveNotificationFeatures.addAll(toGatherFeatures);
                                    try {
                                        traverse(visitor);
                                    } finally {
                                        ignoreResolveNotificationFeatures.removeAll(toGatherFeatures);
                                    }
                                    return null;
                                }
                            };

                        }
                    }
                }
            } catch (Exception e) {
                notifyFatalListener(
                        "EMF-IncQuery Base encountered an error while traversing the EMF model to gather new information. ",
                        e);
                throw new InvocationTargetException(e);
            }
        }
        return finalResult;
    }

    private void traverse(final NavigationHelperVisitor visitor) {
        // Cloning model roots avoids a concurrent modification exception
        for (Notifier root : new HashSet<Notifier>(modelRoots)) {
            comprehension.traverseModel(visitor, root);
        }
        contentAdapter.notifyBaseIndexChangeListeners();
    }

    @Override
    public void addRoot(Notifier emfRoot) throws IncQueryBaseException {
        addRootInternal(emfRoot);
    }

    /* (non-Javadoc)
     * @see org.eclipse.incquery.runtime.base.api.NavigationHelper#cheapMoveTo(org.eclipse.emf.ecore.EObject, org.eclipse.emf.common.util.EList)
     */
    @Override
    public <T extends EObject> void cheapMoveTo(T element, EList<T> targetContainmentReferenceList) {
        if (element.eAdapters().contains(contentAdapter)
                && targetContainmentReferenceList instanceof NotifyingList<?>) {
            final Object listNotifier = ((NotifyingList<?>) targetContainmentReferenceList).getNotifier();
            if (listNotifier instanceof Notifier
                    && ((Notifier) listNotifier).eAdapters().contains(contentAdapter)) {
                contentAdapter.ignoreInsertionAndDeletion = element;
                try {
                    targetContainmentReferenceList.add(element);
                } finally {
                    contentAdapter.ignoreInsertionAndDeletion = null;
                }
            } else {
                targetContainmentReferenceList.add(element);
            }
        } else {
            targetContainmentReferenceList.add(element);
        }
    }

    /* (non-Javadoc)
     * @see org.eclipse.incquery.runtime.base.api.NavigationHelper#cheapMoveTo(org.eclipse.emf.ecore.EObject, org.eclipse.emf.ecore.EObject, org.eclipse.emf.ecore.EReference)
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public void cheapMoveTo(EObject element, EObject parent, EReference containmentFeature) {
        contentAdapter.maintainMetamodel(containmentFeature);
        if (containmentFeature.isMany())
            cheapMoveTo(element, (EList) parent.eGet(containmentFeature));
        else if (element.eAdapters().contains(contentAdapter) && parent.eAdapters().contains(contentAdapter)) {
            contentAdapter.ignoreInsertionAndDeletion = element;
            try {
                parent.eSet(containmentFeature, element);
            } finally {
                contentAdapter.ignoreInsertionAndDeletion = null;
            }
        } else {
            parent.eSet(containmentFeature, element);
        }
    }

    /**
     * @param emfRoot
     * @throws IncQueryBaseException
     */
    private void addRootInternal(Notifier emfRoot) throws IncQueryBaseException {
        if (!((emfRoot instanceof EObject) || (emfRoot instanceof Resource) || (emfRoot instanceof ResourceSet))) {
            throw new IncQueryBaseException(IncQueryBaseException.INVALID_EMFROOT);
        }
        expandToAdditionalRoot(emfRoot);
    }

    @Override
    public Set<EClass> getAllCurrentClasses() {
        return contentAdapter.getAllCurrentClasses();
    }

    protected void processingError(Throwable ex, String task) {
        contentAdapter.processingFatal(ex, task);
    }

    private void ensureNotInWildcardMode() {
        if (inWildcardMode) {
            throw new IllegalStateException("Cannot register/unregister observed classes in wildcard mode");
        }
    }

    private <X, Y> void ensureNoListeners(Set<Object> unobservedTypes,
            final Table<Object, X, Set<Y>> listenerRegistry) {
        if (!Collections.disjoint(unobservedTypes, listenerRegistry.rowKeySet()))
            throw new IllegalStateException(
                    "Cannot unregister observed types for which there are active listeners");
    }

    private void ensureNoListenersForDispose() {
        if (!(baseIndexChangeListeners.isEmpty() && subscribedFeatureListeners.isEmpty()
                && subscribedDataTypeListeners.isEmpty() && subscribedInstanceListeners.isEmpty()))
            throw new IllegalStateException("Cannot dispose while there are active listeners");
    }

    /**
     * Resamples the values of not well-behaving derived features if those features are also indexed.
     */
    public void resampleDerivedFeatures() {
        // otherwise notifications are delivered anyway
        if (!baseIndexOptions.isTraverseOnlyWellBehavingDerivedFeatures()) {
            // get all required classes
            Set<EClass> allCurrentClasses = contentAdapter.getAllCurrentClasses();
            Set<EStructuralFeature> featuresToSample = Sets.newHashSet();
            // collect features to sample
            for (EClass cls : allCurrentClasses) {
                EList<EStructuralFeature> features = cls.getEAllStructuralFeatures();
                for (EStructuralFeature f : features) {
                    // is feature only sampled?
                    if (comprehension.onlySamplingFeature(f)) {
                        featuresToSample.add(f);
                    }
                }
            }

            final EMFVisitor removalVisitor = contentAdapter.visitor(false);
            final EMFVisitor insertionVisitor = contentAdapter.visitor(true);

            // iterate on instances
            for (final EStructuralFeature f : featuresToSample) {
                EClass containingClass = f.getEContainingClass();
                processAllInstances(containingClass, new IEClassProcessor() {
                    @Override
                    public void process(EClass type, EObject instance) {
                        contentAdapter.resampleFeatureValueForHolder(instance, f, insertionVisitor, removalVisitor);
                    }
                });
            }
            contentAdapter.notifyBaseIndexChangeListeners();
        }
    }
}