/*******************************************************************************
 * Copyright (c) 2004, 2018 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
 *******************************************************************************/
package org.eclipse.core.internal.registry;

import java.lang.ref.SoftReference;
import java.util.*;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.spi.RegistryContributor;

/**
 * This class manage all the object from the registry but does not deal with their dependencies.
 * It serves the objects which are either directly obtained from memory or read from a cache.
 * It also returns handles for objects.
 */
public class RegistryObjectManager implements IObjectManager {
	//Constants used to get the objects and their handles
	static public final byte CONFIGURATION_ELEMENT = 1;
	static public final byte EXTENSION = 2;
	static public final byte EXTENSION_POINT = 3;
	static public final byte THIRDLEVEL_CONFIGURATION_ELEMENT = 4;

	static final int CACHE_INITIAL_SIZE = 512; //This value has been picked because it is the minimal size required to startup an RCP app. (FYI, eclipse requires 3 growths).
	static final float DEFAULT_LOADFACTOR = 0.75f; //This is the default factor used in reference map.

	static final int[] EMPTY_INT_ARRAY = new int[0];
	static final String[] EMPTY_STRING_ARRAY = new String[0];

	static final ExtensionHandle[] EMPTY_EXTENSIONS_ARRAY = new ExtensionHandle[0];

	static int UNKNOWN = -1;

	// key: extensionPointName, value: object id
	private HashtableOfStringAndInt extensionPoints; //This is loaded on startup. Then entries can be added when loading a new plugin from the xml.
	// key: object id, value: an object
	private ReferenceMap cache; //Entries are added by getter. The structure is not thread safe.
	//key: int, value: int
	private OffsetTable fileOffsets = null; //This is read once on startup when loading from the cache. Entries are never added here. They are only removed to prevent "removed" objects to be reloaded.

	private int nextId = 1; //This is only used to get the next number available.

	//Those two data structures are only used when the addition or the removal of a plugin occurs.
	//They are used to keep track on a contributor basis of the extension being added or removed
	private final KeyedHashSet newContributions; //represents the contributers added during this session.
	private Object formerContributions; //represents the contributers encountered in previous sessions. This is loaded lazily.

	private HashMap<String, RegistryContributor> contributors; // key: contributor ID; value: contributor name
	private HashMap<String, RegistryContributor> removedContributors; // key: contributor ID; value: contributor name
	private KeyedHashSet namespacesIndex; // registry elements (extension & extensionpoints) indexed by namespaces

	// Map key: extensionPointFullyQualifiedName, value int[] of orphan extensions.
	// The orphan access does not need to be synchronized because the it is protected by the lock in extension registry.
	private Object orphanExtensions;

	private final KeyedHashSet heldObjects = new KeyedHashSet(); //strong reference to the objects that must be hold on to

	//Indicate if objects have been removed or added from the table. This only needs to be set in a couple of places (addNamespace and removeNamespace)
	private boolean isDirty = false;

	private boolean fromCache = false;

	private final ExtensionRegistry registry;

	// TODO this option is not used
	// OSGI system properties.  Copied from EclipseStarter
	public static final String PROP_NO_REGISTRY_FLUSHING = "eclipse.noRegistryFlushing"; //$NON-NLS-1$

	public RegistryObjectManager(ExtensionRegistry registry) {
		extensionPoints = new HashtableOfStringAndInt();
		if ("true".equalsIgnoreCase(RegistryProperties.getProperty(PROP_NO_REGISTRY_FLUSHING))) { //$NON-NLS-1$
			cache = new ReferenceMap(ReferenceMap.HARD, CACHE_INITIAL_SIZE, DEFAULT_LOADFACTOR);
		} else {
			cache = new ReferenceMap(ReferenceMap.SOFT, CACHE_INITIAL_SIZE, DEFAULT_LOADFACTOR);
		}
		newContributions = new KeyedHashSet();

		this.registry = registry;
	}

	/**
	 * Initialize the object manager. Return true if the initialization succeeded, false otherwise
	 */
	synchronized boolean init(long timeStamp) {
		TableReader reader = registry.getTableReader();
		Object[] results = reader.loadTables(timeStamp);
		if (results == null) {
			return false;
		}
		fileOffsets = (OffsetTable) results[0];
		extensionPoints = (HashtableOfStringAndInt) results[1];
		nextId = ((Integer) results[2]).intValue();
		fromCache = true;

		if (!registry.useLazyCacheLoading()) {
			//TODO Here we could grow all the tables to the right size (ReferenceMap)
			reader.setHoldObjects(true);
			markOrphansHasDirty(getOrphans());
			fromCache = reader.readAllCache(this);
			formerContributions = getFormerContributions();
		}
		return fromCache;
	}

	synchronized void addContribution(Contribution contribution) {
		isDirty = true;
		Object Id = contribution.getKey();

		KeyedElement existingContribution = getFormerContributions().getByKey(Id);
		if (existingContribution != null) { // move it from former to new contributions
			removeContribution(Id);
			newContributions.add(existingContribution);
		} else
			existingContribution = newContributions.getByKey(Id);

		if (existingContribution != null) // merge
			((Contribution) existingContribution).mergeContribution(contribution);
		else
			newContributions.add(contribution);

		updateNamespaceIndex(contribution, true);
	}

	// TODO make ExtensionPoint, Extension provide namespace in a same way (move it to the RegistryObject?)
	// See if all the registryObjects have the same namespace. If not, return null.
	// Also can return null if empty array is passed in or objects are of an unexpected type
	private String findCommonNamespaceIdentifier(RegistryObject[] registryObjects) {
		String namespaceName = null;
		for (RegistryObject currentObject : registryObjects) {
			String tmp = null;
			if (currentObject instanceof ExtensionPoint)
				tmp = ((ExtensionPoint) currentObject).getNamespace();
			else if (currentObject instanceof Extension)
				tmp = ((Extension) currentObject).getNamespaceIdentifier();

			if (namespaceName == null) {
				namespaceName = tmp;
				continue;
			}
			if (!namespaceName.equals(tmp)) {
				return null;
			}
		}
		return namespaceName;
	}

	synchronized void removeExtensionPointFromNamespaceIndex(int extensionPoint, String namespaceName) {
		RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
		indexElement.updateExtensionPoint(extensionPoint, false);
	}

	synchronized void removeExtensionFromNamespaceIndex(int extensions, String namespaceName) {
		RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
		indexElement.updateExtension(extensions, false);
	}

	// Called from a synchronized method
	private void updateNamespaceIndex(Contribution contribution, boolean added) {
		// if all extension points are from the same namespace combine them in one block and add them all together
		int[] contribExtensionPoints = contribution.getExtensionPoints();
		RegistryObject[] extensionPointObjects = getObjects(contribExtensionPoints, EXTENSION_POINT);
		String commonExptsNamespace = null;
		if (contribExtensionPoints.length > 1)
			commonExptsNamespace = findCommonNamespaceIdentifier(extensionPointObjects);
		if (commonExptsNamespace != null) {
			RegistryIndexElement indexElement = getNamespaceIndex(commonExptsNamespace);
			indexElement.updateExtensionPoints(contribExtensionPoints, added);
		} else {
			for (int i = 0; i < contribExtensionPoints.length; i++) {
				String namespaceName = ((ExtensionPoint) extensionPointObjects[i]).getNamespace();
				RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
				indexElement.updateExtensionPoint(contribExtensionPoints[i], added);
			}
		}

		// if all extensions are from the same namespace combine them in one block and add them all together
		int[] contrExtensions = contribution.getExtensions();
		RegistryObject[] extensionObjects = getObjects(contrExtensions, EXTENSION);
		String commonExtNamespace = null;
		if (contrExtensions.length > 1)
			commonExtNamespace = findCommonNamespaceIdentifier(extensionObjects);
		if (commonExtNamespace != null) {
			RegistryIndexElement indexElement = getNamespaceIndex(commonExtNamespace);
			indexElement.updateExtensions(contrExtensions, added);
		} else {
			for (int i = 0; i < contrExtensions.length; i++) {
				String namespaceName = ((Extension) extensionObjects[i]).getNamespaceIdentifier();
				RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
				indexElement.updateExtension(contrExtensions[i], added);
			}
		}
	}

	synchronized int[] getExtensionPointsFrom(String id) {
		KeyedElement tmp = newContributions.getByKey(id);
		if (tmp == null)
			tmp = getFormerContributions().getByKey(id);
		if (tmp == null)
			return EMPTY_INT_ARRAY;
		return ((Contribution) tmp).getExtensionPoints();
	}

	synchronized boolean hasContribution(String id) {
		Object result = newContributions.getByKey(id);
		if (result == null)
			result = getFormerContributions().getByKey(id);
		return result != null;
	}

	private KeyedHashSet getFormerContributions() {
		KeyedHashSet result;
		if (fromCache == false)
			return new KeyedHashSet(0);

		if (formerContributions == null || (result = ((KeyedHashSet) ((formerContributions instanceof SoftReference) ? ((SoftReference<?>) formerContributions).get() : formerContributions))) == null) {
			result = registry.getTableReader().loadContributions();
			formerContributions = new SoftReference<>(result);
		}
		return result;
	}

	synchronized public void add(RegistryObject registryObject, boolean hold) {
		if (registryObject.getObjectId() == UNKNOWN) {
			int id = nextId++;
			registryObject.setObjectId(id);
		}
		cache.put(registryObject.getObjectId(), registryObject);
		if (hold)
			hold(registryObject);
	}

	private void remove(RegistryObject registryObject, boolean release) {
		cache.remove(registryObject.getObjectId());
		if (release)
			release(registryObject);
	}

	synchronized void remove(int id, boolean release) {
		RegistryObject toRemove = (RegistryObject) cache.get(id);
		if (fileOffsets != null)
			fileOffsets.removeKey(id);
		if (toRemove != null)
			remove(toRemove, release);
	}

	private void hold(RegistryObject toHold) {
		heldObjects.add(toHold);
	}

	private void release(RegistryObject toRelease) {
		heldObjects.remove(toRelease);
	}

	@Override
	public synchronized Object getObject(int id, byte type) {
		return basicGetObject(id, type);
	}

	private Object basicGetObject(int id, byte type) {
		Object result = cache.get(id);
		if (result != null)
			return result;
		if (fromCache)
			result = load(id, type);
		if (result == null)
			throw new InvalidRegistryObjectException();
		cache.put(id, result);
		return result;
	}

	// The current impementation of this method assumes that we don't cache dynamic
	// extension. In this case all extensions not yet loaded (i.e. not in the memory cache)
	// are "not dynamic" and we actually check memory objects to see if they are dynamic.
	//
	// If we decide to allow caching of dynamic objects, the implementation
	// of this method would have to retrieved the object from disk and check
	// its "dynamic" status. The problem is that id alone is not enough to get the object
	// from the disk; object type is needed as well.
	public boolean shouldPersist(int id) {
		Object result = cache.get(id);
		if (result != null)
			return ((RegistryObject) result).shouldPersist();
		return true;
	}

	@Override
	public synchronized RegistryObject[] getObjects(int[] values, byte type) {
		if (values.length == 0) {
			switch (type) {
				case EXTENSION_POINT :
					return ExtensionPoint.EMPTY_ARRAY;
				case EXTENSION :
					return Extension.EMPTY_ARRAY;
				case CONFIGURATION_ELEMENT :
				case THIRDLEVEL_CONFIGURATION_ELEMENT :
					return ConfigurationElement.EMPTY_ARRAY;
			}
		}

		RegistryObject[] results = null;
		switch (type) {
			case EXTENSION_POINT :
				results = new ExtensionPoint[values.length];
				break;
			case EXTENSION :
				results = new Extension[values.length];
				break;
			case CONFIGURATION_ELEMENT :
			case THIRDLEVEL_CONFIGURATION_ELEMENT :
				results = new ConfigurationElement[values.length];
				break;
		}
		for (int i = 0; i < values.length; i++) {
			results[i] = (RegistryObject) basicGetObject(values[i], type);
		}
		return results;
	}

	synchronized ExtensionPoint getExtensionPointObject(String xptUniqueId) {
		int id;
		if ((id = extensionPoints.get(xptUniqueId)) == HashtableOfStringAndInt.MISSING_ELEMENT)
			return null;
		return (ExtensionPoint) getObject(id, EXTENSION_POINT);
	}

	@Override
	public Handle getHandle(int id, byte type) {
		switch (type) {
			case EXTENSION_POINT :
				return new ExtensionPointHandle(this, id);

			case EXTENSION :
				return new ExtensionHandle(this, id);

			case CONFIGURATION_ELEMENT :
				return new ConfigurationElementHandle(this, id);

			case THIRDLEVEL_CONFIGURATION_ELEMENT :
			default : //avoid compiler error, type should always be known
				return new ThirdLevelConfigurationElementHandle(this, id);
		}
	}

	@Override
	public Handle[] getHandles(int[] ids, byte type) {
		Handle[] results = null;
		int nbrId = ids.length;
		switch (type) {
			case EXTENSION_POINT :
				if (nbrId == 0)
					return ExtensionPointHandle.EMPTY_ARRAY;
				results = new ExtensionPointHandle[nbrId];
				for (int i = 0; i < nbrId; i++) {
					results[i] = new ExtensionPointHandle(this, ids[i]);
				}
				break;

			case EXTENSION :
				if (nbrId == 0)
					return ExtensionHandle.EMPTY_ARRAY;
				results = new ExtensionHandle[nbrId];
				for (int i = 0; i < nbrId; i++) {
					results[i] = new ExtensionHandle(this, ids[i]);
				}
				break;

			case CONFIGURATION_ELEMENT :
				if (nbrId == 0)
					return ConfigurationElementHandle.EMPTY_ARRAY;
				results = new ConfigurationElementHandle[nbrId];
				for (int i = 0; i < nbrId; i++) {
					results[i] = new ConfigurationElementHandle(this, ids[i]);
				}
				break;

			case THIRDLEVEL_CONFIGURATION_ELEMENT :
				if (nbrId == 0)
					return ConfigurationElementHandle.EMPTY_ARRAY;
				results = new ThirdLevelConfigurationElementHandle[nbrId];
				for (int i = 0; i < nbrId; i++) {
					results[i] = new ThirdLevelConfigurationElementHandle(this, ids[i]);
				}
				break;
		}
		return results;
	}

	synchronized ExtensionPointHandle[] getExtensionPointsHandles() {
		return (ExtensionPointHandle[]) getHandles(extensionPoints.getValues(), EXTENSION_POINT);
	}

	synchronized ExtensionPointHandle getExtensionPointHandle(String xptUniqueId) {
		int id = extensionPoints.get(xptUniqueId);
		if (id == HashtableOfStringAndInt.MISSING_ELEMENT)
			return null;
		return (ExtensionPointHandle) getHandle(id, EXTENSION_POINT);
	}

	private Object load(int id, byte type) {
		TableReader reader = registry.getTableReader();
		if (fileOffsets == null)
			return null;
		int offset = fileOffsets.get(id);
		if (offset == Integer.MIN_VALUE)
			return null;
		switch (type) {
			case CONFIGURATION_ELEMENT :
				return reader.loadConfigurationElement(offset);

			case THIRDLEVEL_CONFIGURATION_ELEMENT :
				return reader.loadThirdLevelConfigurationElements(offset, this);

			case EXTENSION :
				return reader.loadExtension(offset);

			case EXTENSION_POINT :
			default : //avoid compile errors. type must always be known
				return reader.loadExtensionPointTree(offset, this);
		}
	}

	synchronized int[] getExtensionsFrom(String contributorId) {
		KeyedElement tmp = newContributions.getByKey(contributorId);
		if (tmp == null)
			tmp = getFormerContributions().getByKey(contributorId);
		if (tmp == null)
			return EMPTY_INT_ARRAY;
		return ((Contribution) tmp).getExtensions();
	}

	synchronized boolean addExtensionPoint(ExtensionPoint currentExtPoint, boolean hold) {
		String uniqueId = currentExtPoint.getUniqueIdentifier();
		if (extensionPoints.get(uniqueId) != HashtableOfStringAndInt.MISSING_ELEMENT)
			return false;
		add(currentExtPoint, hold);
		extensionPoints.put(uniqueId, currentExtPoint.getObjectId());
		return true;
	}

	synchronized void removeExtensionPoint(String extensionPointId) {
		int pointId = extensionPoints.removeKey(extensionPointId);
		if (pointId == HashtableOfStringAndInt.MISSING_ELEMENT)
			return;
		remove(pointId, true);
	}

	public boolean isDirty() {
		return isDirty;
	}

	public void markDirty() {
		isDirty = true;
	}

	synchronized void removeContribution(Object contributorId) {
		boolean removed = newContributions.removeByKey(contributorId);
		if (removed == false) {
			removed = getFormerContributions().removeByKey(contributorId);
			if (removed)
				formerContributions = getFormerContributions(); //This forces the removed namespace to stay around, so we do not forget about removed namespaces
		}

		if (removed) {
			isDirty = true;
			return;
		}

	}

	@SuppressWarnings("unchecked")
	private Map<String, int[]> getOrphans() {
		Object result;
		if (orphanExtensions == null && !fromCache) {
			result = new HashMap<>();
			orphanExtensions = result;
		} else if (orphanExtensions == null || (result = ((orphanExtensions instanceof SoftReference) ? ((SoftReference<?>) orphanExtensions).get() : orphanExtensions)) == null) {
			result = registry.getTableReader().loadOrphans();
			orphanExtensions = new SoftReference<>(result);
		}
		return (HashMap<String, int[]>) result;
	}

	void addOrphans(String extensionPoint, int[] extensions) {
		Map<String, int[]> orphans = getOrphans();
		int[] existingOrphanExtensions = orphans.get(extensionPoint);

		if (existingOrphanExtensions != null) {
			// just add
			int[] newOrphanExtensions = new int[existingOrphanExtensions.length + extensions.length];
			System.arraycopy(existingOrphanExtensions, 0, newOrphanExtensions, 0, existingOrphanExtensions.length);
			System.arraycopy(extensions, 0, newOrphanExtensions, existingOrphanExtensions.length, extensions.length);
			orphans.put(extensionPoint, newOrphanExtensions);
		} else {
			// otherwise this is the first one
			orphans.put(extensionPoint, extensions);
		}
		markOrphansHasDirty(orphans);
	}

	void markOrphansHasDirty(Map<String, int[]> orphans) {
		orphanExtensions = orphans;
	}

	void addOrphan(String extensionPoint, int extension) {
		Map<String, int[]> orphans = getOrphans();
		int[] existingOrphanExtensions = orphans.get(extensionPoint);

		if (existingOrphanExtensions != null) {
			// just add
			int[] newOrphanExtensions = new int[existingOrphanExtensions.length + 1];
			System.arraycopy(existingOrphanExtensions, 0, newOrphanExtensions, 0, existingOrphanExtensions.length);
			newOrphanExtensions[existingOrphanExtensions.length] = extension;
			orphans.put(extensionPoint, newOrphanExtensions);
		} else {
			// otherwise this is the first one
			orphans.put(extensionPoint, new int[] {extension});
		}
		markOrphansHasDirty(orphans);
	}

	int[] removeOrphans(String extensionPoint) {
		Map<String, int[]> orphans = getOrphans();
		int[] existingOrphanExtensions = orphans.remove(extensionPoint);
		if (existingOrphanExtensions != null) {
			markOrphansHasDirty(orphans);
		}
		return existingOrphanExtensions;
	}

	void removeOrphan(String extensionPoint, int extension) {
		Map<String, int[]> orphans = getOrphans();
		int[] existingOrphanExtensions = orphans.get(extensionPoint);

		if (existingOrphanExtensions == null)
			return;

		markOrphansHasDirty(orphans);
		int newSize = existingOrphanExtensions.length - 1;
		if (newSize == 0) {
			orphans.remove(extensionPoint);
			return;
		}

		int[] newOrphanExtensions = new int[existingOrphanExtensions.length - 1];
		for (int i = 0, j = 0; i < existingOrphanExtensions.length; i++)
			if (extension != existingOrphanExtensions[i])
				newOrphanExtensions[j++] = existingOrphanExtensions[i];

		orphans.put(extensionPoint, newOrphanExtensions);
		return;
	}

	//This method is only used by the writer to reach in
	Map<String, int[]> getOrphanExtensions() {
		return getOrphans();
	}

	//	This method is only used by the writer to reach in
	int getNextId() {
		return nextId;
	}

	//	This method is only used by the writer to reach in
	HashtableOfStringAndInt getExtensionPoints() {
		return extensionPoints;
	}

	//	This method is only used by the writer to reach in
	KeyedHashSet[] getContributions() {
		return new KeyedHashSet[] {newContributions, getFormerContributions()};
	}

	// This method is used internally and by the writer to reach in. Notice that it doesn't
	// return contributors marked as removed.
	HashMap<String, RegistryContributor> getContributors() {
		if (contributors == null) {
			if (fromCache == false)
				contributors = new HashMap<>();
			else
				contributors = registry.getTableReader().loadContributors();
		}
		return contributors;
	}

	synchronized IContributor[] getContributorsSync() {
		Collection<RegistryContributor> contributorValues = getContributors().values();
		return contributorValues.toArray(new IContributor[contributorValues.size()]);
	}

	synchronized RegistryContributor getContributor(String id) {
		RegistryContributor contributor = getContributors().get(id);
		if (contributor != null)
			return contributor;
		// check if we have it among removed contributors - potentially
		// notification of removals might be processed after the contributor
		// marked as removed:
		if (removedContributors != null)
			return removedContributors.get(id);
		return null;
	}

	// only adds a contributor if it is not already present in the table
	synchronized void addContributor(RegistryContributor newContributor) {
		String key = newContributor.getActualId();
		if (!getContributors().containsKey(key)) {
			isDirty = true;
			if (removedContributors != null)
				removedContributors.remove(key);
			getContributors().put(key, newContributor);
		}
	}

	synchronized void removeContributor(String id) {
		isDirty = true;
		RegistryContributor removed = getContributors().remove(id);
		if (removed != null) {
			if (removedContributors == null)
				removedContributors = new HashMap<>();
			removedContributors.put(id, removed);
		}
	}

	KeyedHashSet getNamespacesIndex() {
		if (namespacesIndex == null) {
			if (fromCache == false)
				namespacesIndex = new KeyedHashSet(0);
			else
				namespacesIndex = registry.getTableReader().loadNamespaces();
		}
		return namespacesIndex;
	}

	// Find or create required index element
	private RegistryIndexElement getNamespaceIndex(String namespaceName) {
		RegistryIndexElement indexElement = (RegistryIndexElement) getNamespacesIndex().getByKey(namespaceName);
		if (indexElement == null) {
			indexElement = new RegistryIndexElement(namespaceName);
			namespacesIndex.add(indexElement);
		}
		return indexElement;
	}

	/**
	 * Collect all the objects that are removed by this operation and store
	 * them in a IObjectManager so that they can be accessed from the appropriate
	 * deltas but not from the registry.
	 */
	synchronized Map<Integer, RegistryObject> getAssociatedObjects(String contributionId) {
		//Collect all the objects associated with this contribution
		int[] xpts = getExtensionPointsFrom(contributionId);
		int[] exts = getExtensionsFrom(contributionId);
		Map<Integer, RegistryObject> actualObjects = new HashMap<>(xpts.length + exts.length);
		for (int ext : exts) {
			Extension tmp = (Extension) basicGetObject(ext, RegistryObjectManager.EXTENSION);
			actualObjects.put(Integer.valueOf(ext), tmp);
			collectChildren(tmp, 0, actualObjects);
		}
		for (int xpt2 : xpts) {
			ExtensionPoint xpt = (ExtensionPoint) basicGetObject(xpt2, RegistryObjectManager.EXTENSION_POINT);
			actualObjects.put(Integer.valueOf(xpt2), xpt);
		}

		return actualObjects;
	}

	/**
	 * Adds elements to be removed along with the registry object.
	 */
	synchronized void addAssociatedObjects(Map<Integer, RegistryObject> map, RegistryObject registryObject) {
		collectChildren(registryObject, 0, map);
	}

	/**
	 * Add to the set of the objects all extensions and extension points that
	 * could be navigated to from the objects in the set.
	 */
	synchronized void addNavigableObjects(Map<Integer, RegistryObject> associatedObjects) {
		Map<Integer, RegistryObject> result = new HashMap<>();
		for (RegistryObject object : associatedObjects.values()) {
			if (object instanceof Extension) {
				// add extension point
				ExtensionPoint extPoint = getExtensionPointObject(((Extension) object).getExtensionPointIdentifier());
				if (extPoint == null) // already removed?
					continue;

				Integer extPointIndex = Integer.valueOf(extPoint.getKeyHashCode());
				if (!associatedObjects.containsKey(extPointIndex))
					result.put(Integer.valueOf(extPoint.getKeyHashCode()), extPoint);

				// add all extensions for the extension point
				for (int childId : extPoint.getRawChildren()) {
					Extension tmp = (Extension) basicGetObject(childId, RegistryObjectManager.EXTENSION);
					if (tmp == null) // already removed
						continue;
					Integer extensionIndex = Integer.valueOf(childId);
					if (!associatedObjects.containsKey(extensionIndex)) {
						result.put(extensionIndex, tmp);
						collectChildren(tmp, 0, result);
					}
				}
			} else if (object instanceof ExtensionPoint) {
				// by now extensions of this extension point have been marked as orphans
				Map<String, int[]> orphans = getOrphans();
				String name = ((ExtensionPoint) object).getUniqueIdentifier();
				int[] extensions = orphans.get(name);
				if (extensions != null) {
					for (int orphanId : extensions) {
						Extension tmp = (Extension) basicGetObject(orphanId, RegistryObjectManager.EXTENSION);
						if (tmp == null) // already removed
							continue;
						Integer extensionIndex = Integer.valueOf(orphanId);
						if (!associatedObjects.containsKey(extensionIndex)) {
							result.put(extensionIndex, tmp);
							collectChildren(tmp, 0, result);
						}
					}
				}
			}
		}
		associatedObjects.putAll(result);
	}

	synchronized void removeObjects(Map<?, ?> associatedObjects) {
		//Remove the objects from the main object manager so they can no longer be accessed.
		for (Object registryObject : associatedObjects.values()) {
			RegistryObject toRemove = (RegistryObject) registryObject;
			remove((toRemove).getObjectId(), true);
			if (toRemove instanceof ExtensionPoint)
				removeExtensionPoint(((ExtensionPoint) toRemove).getUniqueIdentifier());
		}
	}

	IObjectManager createDelegatingObjectManager(Map<?, ?> object) {
		return new TemporaryObjectManager(object, this);
	}

	private void collectChildren(RegistryObject ce, int level, Map<Integer, RegistryObject> collector) {
		ConfigurationElement[] children = (ConfigurationElement[]) getObjects(ce.getRawChildren(), level == 0 || ce.noExtraData() ? RegistryObjectManager.CONFIGURATION_ELEMENT : RegistryObjectManager.THIRDLEVEL_CONFIGURATION_ELEMENT);
		for (ConfigurationElement child : children) {
			collector.put(Integer.valueOf(child.getObjectId()), child);
			collectChildren(child, level + 1, collector);
		}
	}

	@Override
	public void close() {
		//do nothing.
	}

	public ExtensionRegistry getRegistry() {
		return registry;
	}

	// Called from a synchronized method only
	private boolean unlinkChildFromContributions(KeyedElement[] contributions, int id) {
		for (KeyedElement contribution : contributions) {
			Contribution candidate = (Contribution) contribution;
			if (candidate == null)
				continue;
			if (candidate.hasChild(id)) {
				candidate.unlinkChild(id);
				if (candidate.isEmpty())
					removeContribution(candidate.getContributorId());
				return true;
			}
		}
		return false;
	}

	synchronized boolean unlinkChildFromContributions(int id) {
		if (unlinkChildFromContributions(newContributions.elements, id))
			return true;
		return unlinkChildFromContributions(getFormerContributions().elements, id);
	}

	synchronized public ExtensionPointHandle[] getExtensionPointsFromNamespace(String namespaceName) {
		RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
		int[] namespaceExtensionPoints = indexElement.getExtensionPoints();
		return (ExtensionPointHandle[]) getHandles(namespaceExtensionPoints, EXTENSION_POINT);
	}

	// This method filters out extensions with no extension point
	synchronized public ExtensionHandle[] getExtensionsFromNamespace(String namespaceName) {
		RegistryIndexElement indexElement = getNamespaceIndex(namespaceName);
		int[] namespaceExtensions = indexElement.getExtensions();

		// filter extensions with no extension point (orphan extensions)
		List<Handle> tmp = new ArrayList<>();
		Extension[] exts = (Extension[]) getObjects(namespaceExtensions, EXTENSION);
		for (Extension ext : exts) {
			if (getExtensionPointObject(ext.getExtensionPointIdentifier()) != null) {
				tmp.add(getHandle(ext.getObjectId(), EXTENSION));
			}
		}
		if (tmp.size() == 0)
			return EMPTY_EXTENSIONS_ARRAY;
		ExtensionHandle[] result = new ExtensionHandle[tmp.size()];
		return tmp.toArray(result);
	}

	public ExtensionHandle[] getExtensionsFromContributor(String contributorId) {
		int[] ids = getExtensionsFrom(contributorId); // never null
		return (ExtensionHandle[]) getHandles(ids, RegistryObjectManager.EXTENSION);
	}

	public ExtensionPointHandle[] getExtensionPointsFromContributor(String contributorId) {
		int[] ids = getExtensionPointsFrom(contributorId); // never null
		return (ExtensionPointHandle[]) getHandles(ids, RegistryObjectManager.EXTENSION_POINT);
	}
}
