/*******************************************************************************
 * Copyright (c) 2000, 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
 *     Danail Nachev - exception handling for registry listeners (bug 188369)
 *******************************************************************************/
package org.eclipse.core.internal.registry;

import java.io.*;
import java.lang.reflect.Array;
import java.util.*;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.core.internal.registry.spi.ConfigurationElementAttribute;
import org.eclipse.core.internal.registry.spi.ConfigurationElementDescription;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.spi.*;
import org.eclipse.osgi.storagemanager.StorageManager;
import org.eclipse.osgi.util.NLS;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * An implementation for the extension registry API.
 */
public class ExtensionRegistry implements IExtensionRegistry, IDynamicExtensionRegistry {

	protected class ListenerInfo {
		public String filter;
		public EventListener listener;

		public ListenerInfo(EventListener listener, String filter) {
			this.listener = listener;
			this.filter = filter;
		}

		/**
		 * Used by ListenerList to ensure uniqueness.
		 */
		@Override
		public boolean equals(Object another) {
			return another instanceof ListenerInfo && ((ListenerInfo) another).listener == this.listener;
		}

		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return listener == null ? 0 : listener.hashCode();
		}
	}

	// used to enforce concurrent access policy for readers/writers
	private final ReadWriteMonitor access = new ReadWriteMonitor();

	// deltas not broadcasted yet. Deltas are kept organized by the namespace name (objects with the same namespace are grouped together)
	private transient Map<String, Object> deltas = new HashMap<>(11);

	//storage manager associated with the registry cache
	protected StorageManager cacheStorageManager;

	// all registry change listeners
	private transient ListenerList<ListenerInfo> listeners = new ListenerList<>();

	private RegistryObjectManager registryObjects = null;

	// Table reader associated with this extension registry
	protected TableReader theTableReader = new TableReader(this);

	private final Object masterToken; // use to get full control of the registry; objects created as "static"
	private final Object userToken; // use to modify non-persisted registry elements

	protected RegistryStrategy strategy; // overridable portions of the registry functionality

	private final RegistryTimestamp aggregatedTimestamp = new RegistryTimestamp(); // tracks current contents of the registry

	// encapsulates processing of new registry deltas
	private CombinedEventDelta eventDelta = null;
	// marks a new extended delta. The namespace that normally would not exists is used for this purpose
	private final static String notNamespace = ""; //$NON-NLS-1$

	// does this instance of the extension registry has multiple language support enabled?
	private final boolean isMultiLanguage;

	// have we already logged a error on usage of an unsupported multi-language method?
	private boolean mlErrorLogged = false;

	public RegistryObjectManager getObjectManager() {
		return registryObjects;
	}

	/**
	 * Sets new cache file manager. If existing file manager was owned by the registry,
	 * closes it.
	 *
	 * @param cacheBase the base location for the registry cache
	 * @param isCacheReadOnly whether the file cache is read only
	 */
	protected void setFileManager(File cacheBase, boolean isCacheReadOnly) {
		if (cacheStorageManager != null)
			cacheStorageManager.close(); // close existing file manager first

		if (cacheBase != null) {
			cacheStorageManager = new StorageManager(cacheBase, isCacheReadOnly ? "none" : null, isCacheReadOnly); //$NON-NLS-1$
			try {
				cacheStorageManager.open(!isCacheReadOnly);
			} catch (IOException e) {
				// Ignore the exception. The registry will be rebuilt from source.
			}
		}
	}

	/**
	 * Adds and resolves all extensions and extension points provided by the
	 * plug-in.
	 * <p>
	 * A corresponding IRegistryChangeEvent will be broadcast to all listeners
	 * interested on changes in the given plug-in.
	 * </p>
	 */
	private void add(Contribution element) {
		access.enterWrite();
		try {
			eventDelta = CombinedEventDelta.recordAddition();
			basicAdd(element, true);
			fireRegistryChangeEvent();
			eventDelta = null;
		} finally {
			access.exitWrite();
		}
	}

	/* Utility method to help with array concatenations */
	static Object concatArrays(Object a, Object b) {
		Object[] result = (Object[]) Array.newInstance(a.getClass().getComponentType(), Array.getLength(a) + Array.getLength(b));
		System.arraycopy(a, 0, result, 0, Array.getLength(a));
		System.arraycopy(b, 0, result, Array.getLength(a), Array.getLength(b));
		return result;
	}

	private String addExtension(int extension) {
		Extension addedExtension = (Extension) registryObjects.getObject(extension, RegistryObjectManager.EXTENSION);
		String extensionPointToAddTo = addedExtension.getExtensionPointIdentifier();
		ExtensionPoint extPoint = registryObjects.getExtensionPointObject(extensionPointToAddTo);
		//orphan extension
		if (extPoint == null) {
			registryObjects.addOrphan(extensionPointToAddTo, extension);
			return null;
		}
		// otherwise, link them
		int[] newExtensions;
		int[] existingExtensions = extPoint.getRawChildren();
		newExtensions = new int[existingExtensions.length + 1];
		System.arraycopy(existingExtensions, 0, newExtensions, 0, existingExtensions.length);
		newExtensions[newExtensions.length - 1] = extension;
		link(extPoint, newExtensions);
		if (eventDelta != null)
			eventDelta.rememberExtension(extPoint, extension);
		return recordChange(extPoint, extension, IExtensionDelta.ADDED);
	}

	/**
	 * Looks for existing orphan extensions to connect to the given extension
	 * point. If none is found, there is nothing to do. Otherwise, link them.
	 */
	private String addExtensionPoint(int extPoint) {
		ExtensionPoint extensionPoint = (ExtensionPoint) registryObjects.getObject(extPoint, RegistryObjectManager.EXTENSION_POINT);
		if (eventDelta != null)
			eventDelta.rememberExtensionPoint(extensionPoint);
		int[] orphans = registryObjects.removeOrphans(extensionPoint.getUniqueIdentifier());
		if (orphans == null)
			return null;
		link(extensionPoint, orphans);
		if (eventDelta != null)
			eventDelta.rememberExtensions(extensionPoint, orphans);
		return recordChange(extensionPoint, orphans, IExtensionDelta.ADDED);
	}

	private Set<String> addExtensionsAndExtensionPoints(Contribution element) {
		// now add and resolve extensions and extension points
		Set<String> affectedNamespaces = new HashSet<>();
		for (int extPoint : element.getExtensionPoints()) {
			String namespace = this.addExtensionPoint(extPoint);
			if (namespace != null)
				affectedNamespaces.add(namespace);
		}
		for (int extension : element.getExtensions()) {
			String namespace = this.addExtension(extension);
			if (namespace != null)
				affectedNamespaces.add(namespace);
		}
		return affectedNamespaces;
	}

	@Override
	public void addListener(IRegistryEventListener listener) {
		addListenerInternal(listener, null);
	}

	@Override
	public void addListener(IRegistryEventListener listener, String extensionPointId) {
		addListenerInternal(listener, extensionPointId);
	}

	private void addListenerInternal(EventListener listener, String filter) {
		synchronized (listeners) {
			listeners.add(new ListenerInfo(listener, filter));
		}
	}

	@Override
	public void addRegistryChangeListener(IRegistryChangeListener listener) {
		// this is just a convenience API - no need to do any sync'ing here
		addListenerInternal(listener, null);
	}

	@Override
	public void addRegistryChangeListener(IRegistryChangeListener listener, String filter) {
		addListenerInternal(listener, filter);
	}

	private void basicAdd(Contribution element, boolean link) {
		registryObjects.addContribution(element);
		if (!link)
			return;
		Set<String> affectedNamespaces = addExtensionsAndExtensionPoints(element);
		setObjectManagers(affectedNamespaces, registryObjects.createDelegatingObjectManager(registryObjects.getAssociatedObjects(element.getContributorId())));
	}

	private void setObjectManagers(Set<String> affectedNamespaces, IObjectManager manager) {
		for (String namespace : affectedNamespaces) {
			getDelta(namespace).setObjectManager(manager);
		}
		if (eventDelta != null)
			eventDelta.setObjectManager(manager);
	}

	private void basicRemove(String contributorId) {
		// ignore anonymous namespaces
		Set<String> affectedNamespaces = removeExtensionsAndExtensionPoints(contributorId);
		Map<Integer, RegistryObject> associatedObjects = registryObjects.getAssociatedObjects(contributorId);
		registryObjects.removeObjects(associatedObjects);
		registryObjects.addNavigableObjects(associatedObjects); // put the complete set of navigable objects
		setObjectManagers(affectedNamespaces, registryObjects.createDelegatingObjectManager(associatedObjects));

		registryObjects.removeContribution(contributorId);
		registryObjects.removeContributor(contributorId);
	}

	// allow other objects in the registry to use the same lock
	void enterRead() {
		access.enterRead();
	}

	// allow other objects in the registry to use the same lock
	void exitRead() {
		access.exitRead();
	}

	/**
	 * Broadcasts (asynchronously) the event to all interested parties.
	 */
	private void fireRegistryChangeEvent() {
		// pack new extended delta together with the rest of deltas using invalid namespace
		deltas.put(notNamespace, eventDelta);
		// if there is nothing to say, just bail out
		if (listeners.isEmpty()) {
			deltas.clear();
			return;
		}
		// for thread safety, create tmp collections
		Object[] tmpListeners = listeners.getListeners();
		Map<String, Object> tmpDeltas = new HashMap<>(this.deltas);
		// the deltas have been saved for notification - we can clear them now
		deltas.clear();
		// do the notification asynchronously
		strategy.scheduleChangeEvent(tmpListeners, tmpDeltas, this);
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getConfigurationElementsFor(java.lang.String)
	 */
	@Override
	public IConfigurationElement[] getConfigurationElementsFor(String extensionPointId) {
		// this is just a convenience API - no need to do any sync'ing here
		int lastdot = extensionPointId.lastIndexOf('.');
		if (lastdot == -1)
			return new IConfigurationElement[0];
		return getConfigurationElementsFor(extensionPointId.substring(0, lastdot), extensionPointId.substring(lastdot + 1));
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getConfigurationElementsFor(java.lang.String, java.lang.String)
	 */
	@Override
	public IConfigurationElement[] getConfigurationElementsFor(String pluginId, String extensionPointSimpleId) {
		// this is just a convenience API - no need to do any sync'ing here
		IExtensionPoint extPoint = this.getExtensionPoint(pluginId, extensionPointSimpleId);
		if (extPoint == null)
			return new IConfigurationElement[0];
		return extPoint.getConfigurationElements();
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getConfigurationElementsFor(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public IConfigurationElement[] getConfigurationElementsFor(String pluginId, String extensionPointName, String extensionId) {
		// this is just a convenience API - no need to do any sync'ing here
		IExtension extension = this.getExtension(pluginId, extensionPointName, extensionId);
		if (extension == null)
			return new IConfigurationElement[0];
		return extension.getConfigurationElements();
	}

	private RegistryDelta getDelta(String namespace) {
		// is there a delta for the plug-in?
		RegistryDelta existingDelta = (RegistryDelta) deltas.get(namespace);
		if (existingDelta != null)
			return existingDelta;

		//if not, create one
		RegistryDelta delta = new RegistryDelta();
		deltas.put(namespace, delta);
		return delta;
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtension(java.lang.String)
	 */
	@Override
	public IExtension getExtension(String extensionId) {
		if (extensionId == null)
			return null;
		int lastdot = extensionId.lastIndexOf('.');
		if (lastdot == -1)
			return null;
		String namespace = extensionId.substring(0, lastdot);

		ExtensionHandle[] extensions;
		access.enterRead();
		try {
			extensions = registryObjects.getExtensionsFromNamespace(namespace);
		} finally {
			access.exitRead();
		}
		for (ExtensionHandle suspect : extensions) {
			if (extensionId.equals(suspect.getUniqueIdentifier()))
				return suspect;
		}
		return null;
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtension(java.lang.String, java.lang.String)
	 */
	@Override
	public IExtension getExtension(String extensionPointId, String extensionId) {
		// this is just a convenience API - no need to do any sync'ing here
		int lastdot = extensionPointId.lastIndexOf('.');
		if (lastdot == -1)
			return null;
		return getExtension(extensionPointId.substring(0, lastdot), extensionPointId.substring(lastdot + 1), extensionId);
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtension(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public IExtension getExtension(String pluginId, String extensionPointName, String extensionId) {
		// this is just a convenience API - no need to do any sync'ing here
		IExtensionPoint extPoint = getExtensionPoint(pluginId, extensionPointName);
		if (extPoint != null)
			return extPoint.getExtension(extensionId);
		return null;
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtensionPoint(java.lang.String)
	 */
	@Override
	public IExtensionPoint getExtensionPoint(String xptUniqueId) {
		access.enterRead();
		try {
			return registryObjects.getExtensionPointHandle(xptUniqueId);
		} finally {
			access.exitRead();
		}
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtensionPoint(java.lang.String, java.lang.String)
	 */
	@Override
	public IExtensionPoint getExtensionPoint(String elementName, String xpt) {
		access.enterRead();
		try {
			return registryObjects.getExtensionPointHandle(elementName + '.' + xpt);
		} finally {
			access.exitRead();
		}
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtensionPoints()
	 */
	@Override
	public IExtensionPoint[] getExtensionPoints() {
		access.enterRead();
		try {
			return registryObjects.getExtensionPointsHandles();
		} finally {
			access.exitRead();
		}
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtensionPoints(java.lang.String)
	 */
	@Override
	public IExtensionPoint[] getExtensionPoints(String namespaceName) {
		access.enterRead();
		try {
			return registryObjects.getExtensionPointsFromNamespace(namespaceName);
		} finally {
			access.exitRead();
		}
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getExtensions(java.lang.String)
	 */
	@Override
	public IExtension[] getExtensions(String namespaceName) {
		access.enterRead();
		try {
			return registryObjects.getExtensionsFromNamespace(namespaceName);
		} finally {
			access.exitRead();
		}
	}

	@Override
	public IExtension[] getExtensions(IContributor contributor) {
		if (!(contributor instanceof RegistryContributor))
			throw new IllegalArgumentException(); // should never happen
		String contributorId = ((RegistryContributor) contributor).getActualId();
		access.enterRead();
		try {
			return registryObjects.getExtensionsFromContributor(contributorId);
		} finally {
			access.exitRead();
		}
	}

	@Override
	public IExtensionPoint[] getExtensionPoints(IContributor contributor) {
		if (!(contributor instanceof RegistryContributor))
			throw new IllegalArgumentException(); // should never happen
		String contributorId = ((RegistryContributor) contributor).getActualId();
		access.enterRead();
		try {
			return registryObjects.getExtensionPointsFromContributor(contributorId);
		} finally {
			access.exitRead();
		}
	}

	/*
	 *  (non-Javadoc)
	 * @see org.eclipse.core.runtime.IExtensionRegistry#getNamespaces()
	 */
	@Override
	public String[] getNamespaces() {
		access.enterRead();
		try {
			KeyedElement[] namespaceElements = registryObjects.getNamespacesIndex().elements();
			String[] namespaceNames = new String[namespaceElements.length];
			for (int i = 0; i < namespaceElements.length; i++) {
				namespaceNames[i] = (String) ((RegistryIndexElement) namespaceElements[i]).getKey();
			}
			return namespaceNames;
		} finally {
			access.exitRead();
		}
	}

	@Override
	public boolean hasContributor(IContributor contributor) {
		if (!(contributor instanceof RegistryContributor))
			throw new IllegalArgumentException(); // should never happen
		String contributorId = ((RegistryContributor) contributor).getActualId();
		return hasContributor(contributorId);
	}

	public boolean hasContributor(String contributorId) {
		access.enterRead();
		try {
			return registryObjects.hasContribution(contributorId);
		} finally {
			access.exitRead();
		}
	}

	private void link(ExtensionPoint extPoint, int[] extensions) {
		extPoint.setRawChildren(extensions);
		registryObjects.add(extPoint, true);
	}

	/*
	 * Records an extension addition/removal.
	 */
	private String recordChange(ExtensionPoint extPoint, int extension, int kind) {
		// avoid computing deltas when there are no listeners
		if (listeners.isEmpty())
			return null;
		ExtensionDelta extensionDelta = new ExtensionDelta();
		extensionDelta.setExtension(extension);
		extensionDelta.setExtensionPoint(extPoint.getObjectId());
		extensionDelta.setKind(kind);
		getDelta(extPoint.getNamespace()).addExtensionDelta(extensionDelta);
		return extPoint.getNamespace();
	}

	/*
	 * Records a set of extension additions/removals.
	 */
	private String recordChange(ExtensionPoint extPoint, int[] extensions, int kind) {
		if (listeners.isEmpty())
			return null;
		String namespace = extPoint.getNamespace();
		if (extensions == null || extensions.length == 0)
			return namespace;
		RegistryDelta pluginDelta = getDelta(extPoint.getNamespace());
		for (int extension : extensions) {
			ExtensionDelta extensionDelta = new ExtensionDelta();
			extensionDelta.setExtension(extension);
			extensionDelta.setExtensionPoint(extPoint.getObjectId());
			extensionDelta.setKind(kind);
			pluginDelta.addExtensionDelta(extensionDelta);
		}
		return namespace;
	}

	public void remove(String removedContributorId, long timestamp) {
		remove(removedContributorId);
		if (timestamp != 0)
			aggregatedTimestamp.remove(timestamp);
	}

	@Override
	public void removeContributor(IContributor contributor, Object key) {
		if (!(contributor instanceof RegistryContributor))
			throw new IllegalArgumentException(); // should never happen
		if (!checkReadWriteAccess(key, true))
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.removeContributor() method. Check if proper access token is supplied."); //$NON-NLS-1$
		String contributorId = ((RegistryContributor) contributor).getActualId();
		remove(contributorId);
	}

	/**
	 * Unresolves and removes all extensions and extension points provided by
	 * the plug-in.
	 * <p>
	 * A corresponding IRegistryChangeEvent will be broadcast to all listeners
	 * interested on changes in the given plug-in.
	 * </p>
	 */
	public void remove(String removedContributorId) {
		access.enterWrite();
		try {
			eventDelta = CombinedEventDelta.recordRemoval();
			basicRemove(removedContributorId);
			fireRegistryChangeEvent();
			eventDelta = null;
		} finally {
			access.exitWrite();
		}
	}

	//Return the affected namespace
	private String removeExtension(int extensionId) {
		Extension extension = (Extension) registryObjects.getObject(extensionId, RegistryObjectManager.EXTENSION);
		registryObjects.removeExtensionFromNamespaceIndex(extensionId, extension.getNamespaceIdentifier());
		String xptName = extension.getExtensionPointIdentifier();
		ExtensionPoint extPoint = registryObjects.getExtensionPointObject(xptName);
		if (extPoint == null) {
			registryObjects.removeOrphan(xptName, extensionId);
			return null;
		}
		// otherwise, unlink the extension from the extension point
		int[] existingExtensions = extPoint.getRawChildren();
		int[] newExtensions = RegistryObjectManager.EMPTY_INT_ARRAY;
		if (existingExtensions.length > 1) {
			newExtensions = new int[existingExtensions.length - 1];
			for (int i = 0, j = 0; i < existingExtensions.length; i++)
				if (existingExtensions[i] != extension.getObjectId())
					newExtensions[j++] = existingExtensions[i];
		}
		link(extPoint, newExtensions);
		if (eventDelta != null)
			eventDelta.rememberExtension(extPoint, extensionId);
		return recordChange(extPoint, extension.getObjectId(), IExtensionDelta.REMOVED);
	}

	private String removeExtensionPoint(int extPoint) {
		ExtensionPoint extensionPoint = (ExtensionPoint) registryObjects.getObject(extPoint, RegistryObjectManager.EXTENSION_POINT);
		registryObjects.removeExtensionPointFromNamespaceIndex(extPoint, extensionPoint.getNamespace());
		int[] existingExtensions = extensionPoint.getRawChildren();
		if (existingExtensions != null && existingExtensions.length != 0) {
			registryObjects.addOrphans(extensionPoint.getUniqueIdentifier(), existingExtensions);
			link(extensionPoint, RegistryObjectManager.EMPTY_INT_ARRAY);
		}
		if (eventDelta != null) {
			eventDelta.rememberExtensionPoint(extensionPoint);
			eventDelta.rememberExtensions(extensionPoint, existingExtensions);
		}
		return recordChange(extensionPoint, existingExtensions, IExtensionDelta.REMOVED);
	}

	private Set<String> removeExtensionsAndExtensionPoints(String contributorId) {
		Set<String> affectedNamespaces = new HashSet<>();
		for (int extension : registryObjects.getExtensionsFrom(contributorId)) {
			String namespace = this.removeExtension(extension);
			if (namespace != null)
				affectedNamespaces.add(namespace);
		}

		// remove extension points
		for (int extPoint : registryObjects.getExtensionPointsFrom(contributorId)) {
			String namespace = this.removeExtensionPoint(extPoint);
			if (namespace != null)
				affectedNamespaces.add(namespace);
		}
		return affectedNamespaces;
	}

	@Override
	public void removeRegistryChangeListener(IRegistryChangeListener listener) {
		synchronized (listeners) {
			listeners.remove(new ListenerInfo(listener, null));
		}
	}

	@Override
	public void removeListener(IRegistryEventListener listener) {
		synchronized (listeners) {
			listeners.remove(new ListenerInfo(listener, null));
		}
	}

	public ExtensionRegistry(RegistryStrategy registryStrategy, Object masterToken, Object userToken) {
		isMultiLanguage = "true".equals(RegistryProperties.getProperty(IRegistryConstants.PROP_MULTI_LANGUAGE)); //$NON-NLS-1$

		if (registryStrategy != null)
			strategy = registryStrategy;
		else
			strategy = new RegistryStrategy(null, null);

		this.masterToken = masterToken;
		this.userToken = userToken;
		registryObjects = new RegistryObjectManager(this);

		boolean isRegistryFilledFromCache = false; // indicates if registry was able to use cache to populate it's content

		if (strategy.cacheUse()) {
			// Try to read the registry from the cache first. If that fails, create a new registry
			long start = 0;
			if (debug())
				start = System.currentTimeMillis();

			//The cache is made of several files, find the real names of these other files. If all files are found, try to initialize the objectManager
			if (checkCache()) {
				try {
					theTableReader.setTableFile(cacheStorageManager.lookup(TableReader.TABLE, false));
					theTableReader.setExtraDataFile(cacheStorageManager.lookup(TableReader.EXTRA, false));
					theTableReader.setMainDataFile(cacheStorageManager.lookup(TableReader.MAIN, false));
					theTableReader.setContributionsFile(cacheStorageManager.lookup(TableReader.CONTRIBUTIONS, false));
					theTableReader.setContributorsFile(cacheStorageManager.lookup(TableReader.CONTRIBUTORS, false));
					theTableReader.setNamespacesFile(cacheStorageManager.lookup(TableReader.NAMESPACES, false));
					theTableReader.setOrphansFile(cacheStorageManager.lookup(TableReader.ORPHANS, false));
					long timestamp = strategy.getContributionsTimestamp();
					isRegistryFilledFromCache = registryObjects.init(timestamp);
					if (isRegistryFilledFromCache)
						aggregatedTimestamp.set(timestamp);
				} catch (IOException e) {
					// The registry will be rebuilt from the xml files. Make sure to clear anything filled
					// from cache so that we won't have partially filled items.
					isRegistryFilledFromCache = false;
					clearRegistryCache();
					log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, RegistryMessages.registry_bad_cache, e));
				}
			}

			if (!isRegistryFilledFromCache) {
				// set cache storage manager to a first writable location
				for (int index = 0; index < strategy.getLocationsLength(); index++) {
					if (!strategy.isCacheReadOnly(index)) {
						setFileManager(strategy.getStorage(index), false);
						break;
					}
				}
			}

			if (debug() && isRegistryFilledFromCache)
				System.out.println("Reading registry cache: " + (System.currentTimeMillis() - start)); //$NON-NLS-1$

			if (debug()) {
				if (!isRegistryFilledFromCache)
					System.out.println("Reloading registry from manifest files..."); //$NON-NLS-1$
				else
					System.out.println("Using registry cache..."); //$NON-NLS-1$
			}
		}

		if (debugEvents())
			addRegistryChangeListener(System.out::println);

		// Do extra start processing if specified in the registry strategy
		strategy.onStart(this); // preserve for backward compatibility; might be removed later
		strategy.onStart(this, isRegistryFilledFromCache);
	}

	/**
	 * Stops the registry. Registry has to be stopped to properly
	 * close cache and dispose of listeners.
	 * @param key - key token for this registry
	 */
	@Override
	public void stop(Object key) {
		// If the registry creator specified a key token, check that the key mathches it
		// (it is assumed that registry owner keeps the key to prevent unautorized accesss).
		if (masterToken != null && masterToken != key) {
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.stop() method. Check if proper access token is supplied."); //$NON-NLS-1$
		}

		// Do extra stop processing if specified in the registry strategy
		strategy.onStop(this);

		stopChangeEventScheduler();

		if (cacheStorageManager == null)
			return;

		if (!registryObjects.isDirty() || cacheStorageManager.isReadOnly()) {
			cacheStorageManager.close();
			theTableReader.close();
			return;
		}

		File tableFile = null;
		File mainFile = null;
		File extraFile = null;
		File contributionsFile = null;
		File contributorsFile = null;
		File namespacesFile = null;
		File orphansFile = null;

		TableWriter theTableWriter = new TableWriter(this);

		try {
			cacheStorageManager.lookup(TableReader.TABLE, true);
			cacheStorageManager.lookup(TableReader.MAIN, true);
			cacheStorageManager.lookup(TableReader.EXTRA, true);
			cacheStorageManager.lookup(TableReader.CONTRIBUTIONS, true);
			cacheStorageManager.lookup(TableReader.CONTRIBUTORS, true);
			cacheStorageManager.lookup(TableReader.NAMESPACES, true);
			cacheStorageManager.lookup(TableReader.ORPHANS, true);
			tableFile = File.createTempFile(TableReader.TABLE, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			mainFile = File.createTempFile(TableReader.MAIN, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			extraFile = File.createTempFile(TableReader.EXTRA, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			contributionsFile = File.createTempFile(TableReader.CONTRIBUTIONS, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			contributorsFile = File.createTempFile(TableReader.CONTRIBUTORS, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			namespacesFile = File.createTempFile(TableReader.NAMESPACES, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			orphansFile = File.createTempFile(TableReader.ORPHANS, ".new", cacheStorageManager.getBase()); //$NON-NLS-1$
			theTableWriter.setTableFile(tableFile);
			theTableWriter.setExtraDataFile(extraFile);
			theTableWriter.setMainDataFile(mainFile);
			theTableWriter.setContributionsFile(contributionsFile);
			theTableWriter.setContributorsFile(contributorsFile);
			theTableWriter.setNamespacesFile(namespacesFile);
			theTableWriter.setOrphansFile(orphansFile);
		} catch (IOException e) {
			cacheStorageManager.close();
			return; //Ignore the exception since we can recompute the cache
		}
		try {
			long timestamp;
			// A bit of backward compatibility: if registry was modified, but timestamp was not,
			// it means that the new timestamp tracking mechanism was not used. In this case
			// explicitly obtain timestamps for all contributions. Note that this logic
			// maintains a problem described in the bug 104267 for contributions that
			// don't use the timestamp tracking mechanism.
			if (aggregatedTimestamp.isModifed())
				timestamp = aggregatedTimestamp.getContentsTimestamp(); // use timestamp tracking
			else
				timestamp = strategy.getContributionsTimestamp(); // use legacy approach

			if (theTableWriter.saveCache(registryObjects, timestamp))
				cacheStorageManager.update(new String[] {TableReader.TABLE, TableReader.MAIN, TableReader.EXTRA, TableReader.CONTRIBUTIONS, TableReader.CONTRIBUTORS, TableReader.NAMESPACES, TableReader.ORPHANS}, new String[] {tableFile.getName(), mainFile.getName(), extraFile.getName(), contributionsFile.getName(), contributorsFile.getName(), namespacesFile.getName(), orphansFile.getName()});
		} catch (IOException e) {
			//Ignore the exception since we can recompute the cache
		}
		theTableReader.close();
		cacheStorageManager.close();
	}

	/*
	 * Clear the registry cache files from the file manager so on next start-up we recompute it.
	 */
	public void clearRegistryCache() {
		for (String key : new String[] {TableReader.TABLE, TableReader.MAIN, TableReader.EXTRA, TableReader.CONTRIBUTIONS, TableReader.ORPHANS})
			try {
				cacheStorageManager.remove(key);
			} catch (IOException e) {
				log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, IStatus.ERROR, RegistryMessages.meta_registryCacheReadProblems, e));
			}
		aggregatedTimestamp.reset();
	}

	/////////////////////////////////////////////////////////////////////////////////////////////////
	// Registry Object Factory
	// The factory produces contributions, extension points, extensions, and configuration elements
	// to be stored in the extension registry.
	protected RegistryObjectFactory theRegistryObjectFactory = null;

	// Override to provide domain-specific elements to be stored in the extension registry
	protected void setElementFactory() {
		if (isMultiLanguage)
			theRegistryObjectFactory = new RegistryObjectFactoryMulti(this);
		else
			theRegistryObjectFactory = new RegistryObjectFactory(this);
	}

	// Lazy initialization.
	public RegistryObjectFactory getElementFactory() {
		if (theRegistryObjectFactory == null)
			setElementFactory();
		return theRegistryObjectFactory;
	}

	TableReader getTableReader() {
		return theTableReader;
	}

	public void log(IStatus status) {
		strategy.log(status);
	}

	/**
	 * With multi-locale support enabled this method returns the non-translated
	 * key so that they can be cached and translated later into desired languages.
	 * In the absence of the multi-locale support the key gets translated immediately
	 * and only translated values is cached.
	 */
	public String translate(String key, ResourceBundle resources) {
		if (isMultiLanguage)
			return key;
		return strategy.translate(key, resources);
	}

	public boolean debug() {
		return strategy.debug();
	}

	public boolean debugEvents() {
		return strategy.debugRegistryEvents();
	}

	public boolean useLazyCacheLoading() {
		return strategy.cacheLazyLoading();
	}

	public long computeState() {
		return strategy.getContainerTimestamp();
	}

	// Find the first location that contains a cache table file and set file manager to it.
	protected boolean checkCache() {
		for (int index = 0; index < strategy.getLocationsLength(); index++) {
			File possibleCacheLocation = strategy.getStorage(index);
			if (possibleCacheLocation == null)
				break; // bail out on the first null
			setFileManager(possibleCacheLocation, strategy.isCacheReadOnly(index));
			if (cacheStorageManager != null) {
				// check this new location:
				File cacheFile = null;
				try {
					cacheFile = cacheStorageManager.lookup(TableReader.getTestFileName(), false);
				} catch (IOException e) {
					//Ignore the exception. The registry will be rebuilt from the xml files.
				}
				if (cacheFile != null && cacheFile.isFile())
					return true; // found the appropriate location
			}
		}
		return false;
	}

	public Object createExecutableExtension(RegistryContributor defaultContributor, String className, String requestedContributorName) throws CoreException {
		return strategy.createExecutableExtension(defaultContributor, className, requestedContributorName);
	}

	//////////////////////////////////////////////////////////////////////////////////////////
	// Registry change events processing

	public IStatus processChangeEvent(Object[] listenerInfos, final Map<String, ?> scheduledDeltas) {
		// Separate new event delta from the pack
		final CombinedEventDelta extendedDelta = (CombinedEventDelta) scheduledDeltas.remove(notNamespace);

		final MultiStatus result = new MultiStatus(RegistryMessages.OWNER_NAME, IStatus.OK, RegistryMessages.plugin_eventListenerError, null);
		for (Object info : listenerInfos) {
			final ListenerInfo listenerInfo = (ListenerInfo) info;
			if ((listenerInfo.listener instanceof IRegistryChangeListener) && scheduledDeltas.size() != 0) {
				if (listenerInfo.filter == null || scheduledDeltas.containsKey(listenerInfo.filter)) {
					SafeRunner.run(new ISafeRunnable() {
						@Override
						public void run() throws Exception {
							((IRegistryChangeListener) listenerInfo.listener).registryChanged(new RegistryChangeEvent(scheduledDeltas, listenerInfo.filter));
						}

						@Override
						public void handleException(Throwable exception) {
							result.add(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, RegistryMessages.plugin_eventListenerError, exception));
						}
					});
				}
			}
			if (listenerInfo.listener instanceof IRegistryEventListener) {
				IRegistryEventListener extensionListener = (IRegistryEventListener) listenerInfo.listener;
				IExtension[] extensions = extendedDelta.getExtensions(listenerInfo.filter);
				IExtensionPoint[] extensionPoints = extendedDelta.getExtensionPoints(listenerInfo.filter);

				// notification order - on addition: extension points; then extensions
				if (extendedDelta.isAddition()) {
					if (extensionPoints != null)
						extensionListener.added(extensionPoints);
					if (extensions != null)
						extensionListener.added(extensions);
				} else { // on removal: extensions; then extension points
					if (extensions != null)
						extensionListener.removed(extensions);
					if (extensionPoints != null)
						extensionListener.removed(extensionPoints);
				}
			}
		}
		for (Object delta : scheduledDeltas.values()) {
			((RegistryDelta) delta).getObjectManager().close();
		}
		IObjectManager manager = extendedDelta.getObjectManager();
		if (manager != null)
			manager.close();
		return result;
	}

	private RegistryEventThread eventThread = null; // registry event loop
	protected final List<QueueElement> queue = new LinkedList<>(); // stores registry events info

	// Registry events notifications are done on a separate thread in a sequential manner
	// (first in - first processed)
	public void scheduleChangeEvent(Object[] listenerInfos, Map<String, ?> scheduledDeltas) {
		QueueElement newElement = new QueueElement(listenerInfos, scheduledDeltas);
		if (eventThread == null) {
			eventThread = new RegistryEventThread(this);
			eventThread.start();
		}
		synchronized (queue) {
			queue.add(newElement);
			queue.notify();
		}
	}

	// The pair of values we store in the event queue
	private static class QueueElement {
		Object[] listenerInfos;
		Map<String, ?> scheduledDeltas;

		QueueElement(Object[] infos, Map<String, ?> deltas) {
			this.scheduledDeltas = deltas;
			listenerInfos = infos;
		}
	}

	private class RegistryEventThread extends Thread {
		private final ExtensionRegistry registry;

		public RegistryEventThread(ExtensionRegistry registry) {
			super("Extension Registry Event Dispatcher"); //$NON-NLS-1$
			setDaemon(true);
			this.registry = registry;
		}

		@Override
		public void run() {
			while (true) {
				QueueElement element;
				synchronized (queue) {
					try {
						while (queue.isEmpty())
							queue.wait();
					} catch (InterruptedException e) {
						return;
					}
					element = queue.remove(0);
				}
				registry.processChangeEvent(element.listenerInfos, element.scheduledDeltas);
			}
		}
	}

	protected void stopChangeEventScheduler() {
		if (eventThread != null) {
			synchronized (queue) {
				eventThread.interrupt();
				eventThread = null;
			}
		}
	}

	/**
	 * Access check for add/remove operations:
	 * - Master key allows all operations
	 * - User key allows modifications of non-persisted elements
	 *
	 * @param key key to the registry supplied by the user
	 * @param persist true if operation affects persisted elements
	 * @return true is the key grants read/write access to the registry
	 */
	private boolean checkReadWriteAccess(Object key, boolean persist) {
		if (masterToken == key)
			return true;
		if (userToken == key && !persist)
			return true;
		return false;
	}

	public boolean addContribution(InputStream is, IContributor contributor, boolean persist, String contributionName, ResourceBundle translationBundle, Object key, long timestamp) {
		boolean result = addContribution(is, contributor, persist, contributionName, translationBundle, key);
		if (timestamp != 0)
			aggregatedTimestamp.add(timestamp);
		return result;
	}

	@Override
	public boolean addContribution(InputStream is, IContributor contributor, boolean persist, String contributionName, ResourceBundle translationBundle, Object key) {
		if (!checkReadWriteAccess(key, persist))
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.addContribution() method. Check if proper access token is supplied."); //$NON-NLS-1$
		if (contributionName == null)
			contributionName = ""; //$NON-NLS-1$

		RegistryContributor internalContributor = (RegistryContributor) contributor;
		registryObjects.addContributor(internalContributor); // only adds a contributor if it is not already present

		String ownerName = internalContributor.getActualName();
		String message = NLS.bind(RegistryMessages.parse_problems, ownerName);
		MultiStatus problems = new MultiStatus(RegistryMessages.OWNER_NAME, ExtensionsParser.PARSE_PROBLEM, message, null);
		ExtensionsParser parser = new ExtensionsParser(problems, this);
		Contribution contribution = getElementFactory().createContribution(internalContributor.getActualId(), persist);

		try {
			parser.parseManifest(strategy.getXMLParser(), new InputSource(is), contributionName, getObjectManager(), contribution, translationBundle);
			int status = problems.getSeverity();
			if (status != IStatus.OK) {
				log(problems);
				if (status == IStatus.ERROR || status == IStatus.CANCEL)
					return false;
			}
		} catch (ParserConfigurationException | SAXException | IOException e) {
			logError(ownerName, contributionName, e);
			return false;
		} finally {
			try {
				is.close();
			} catch (IOException ioe) {
				// nothing to do
			}
		}
		add(contribution); // the add() method does synchronization
		return true;
	}

	private void logError(String owner, String contributionName, Exception e) {
		String message = NLS.bind(RegistryMessages.parse_failedParsingManifest, owner + "/" + contributionName); //$NON-NLS-1$
		log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, message, e));
	}

	/**
	 * Adds an extension point to the extension registry.
	 * <p>
	 * If the registry is not modifiable, this method is an access controlled method.
	 * Proper token should be passed as an argument for non-modifiable registries.
	 * </p>
	 * @param identifier Id of the extension point. If non-qualified names is supplied,
	 * it will be converted internally into a fully qualified name
	 * @param contributor the contributor of this extension point
	 * @param persist indicates if contribution should be stored in the registry cache. If false,
	 * contribution is not persisted in the registry cache and is lost on Eclipse restart
	 * @param label display string for the extension point
	 * @param schemaReference reference to the extension point schema. The schema reference
	 * is a URL path relative to the plug-in installation URL. May be null
	 * @param token the key used to check permissions. Two registry keys are set in the registry
	 * constructor {@link RegistryFactory#createRegistry(org.eclipse.core.runtime.spi.RegistryStrategy, Object, Object)}:
	 * master token and a user token. Master token allows all operations; user token
	 * allows non-persisted registry elements to be modified.
	 * @return <code>true</code> if successful, <code>false</code> if a problem was encountered
	 * @throws IllegalArgumentException if incorrect token is passed in
	 */
	public boolean addExtensionPoint(String identifier, IContributor contributor, boolean persist, String label, String schemaReference, Object token) throws IllegalArgumentException {
		if (!checkReadWriteAccess(token, persist))
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.addExtensionPoint() method. Check if proper access token is supplied."); //$NON-NLS-1$

		RegistryContributor internalContributor = (RegistryContributor) contributor;
		registryObjects.addContributor(internalContributor); // only adds a contributor if it is not already present
		String contributorId = internalContributor.getActualId();

		// Extension point Id might not be null
		if (identifier == null) {
			String message = NLS.bind(RegistryMessages.create_failedExtensionPoint, label);
			log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, message, null));
			throw new NullPointerException(message);
		}
		if (schemaReference == null)
			schemaReference = ""; //$NON-NLS-1$

		// addition wraps in a contribution
		Contribution contribution = getElementFactory().createContribution(contributorId, persist);
		ExtensionPoint currentExtPoint = getElementFactory().createExtensionPoint(persist);

		String uniqueId;
		String namespaceName;
		int simpleIdStart = identifier.lastIndexOf('.');
		if (simpleIdStart == -1) {
			namespaceName = contribution.getDefaultNamespace();
			uniqueId = namespaceName + '.' + identifier;
		} else {
			namespaceName = identifier.substring(0, simpleIdStart);
			uniqueId = identifier;
		}
		currentExtPoint.setUniqueIdentifier(uniqueId);
		currentExtPoint.setNamespace(namespaceName);
		String labelNLS = translate(label, null);
		currentExtPoint.setLabel(labelNLS);
		currentExtPoint.setSchema(schemaReference);

		if (!getObjectManager().addExtensionPoint(currentExtPoint, true)) {
			if (debug()) {
				String msg = NLS.bind(RegistryMessages.parse_duplicateExtensionPoint, uniqueId, contribution.getDefaultNamespace());
				log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, msg, null));
			}
			return false;
		}

		currentExtPoint.setContributorId(contributorId);

		// array format: {Number of extension points, Number of extensions, Extension Id}
		int[] contributionChildren = new int[3];
		// Put the extension points into this namespace
		contributionChildren[Contribution.EXTENSION_POINT] = 1;
		contributionChildren[Contribution.EXTENSION] = 0;
		contributionChildren[Contribution.EXTENSION + 1] = currentExtPoint.getObjectId();

		contribution.setRawChildren(contributionChildren);

		add(contribution);
		return true;
	}

	/**
	 * Adds an extension to the extension registry.
	 * <p>
	 * If the registry is not modifiable, this method is an access controlled method.
	 * Proper token should be passed as an argument for non-modifiable registries.
	 * </p>
	 * @see org.eclipse.core.internal.registry.spi.ConfigurationElementDescription
	 *
	 * @param identifier Id of the extension. If non-qualified name is supplied,
	 * it will be converted internally into a fully qualified name
	 * @param contributor the contributor of this extension
	 * @param persist indicates if contribution should be stored in the registry cache. If false,
	 * contribution is not persisted in the registry cache and is lost on Eclipse restart
	 * @param label display string for this extension
	 * @param extensionPointId Id of the point being extended. If non-qualified
	 * name is supplied, it is assumed to have the same contributorId as this extension
	 * @param configurationElements contents of the extension
	 * @param token the key used to check permissions. Two registry keys are set in the registry
	 * constructor {@link RegistryFactory#createRegistry(org.eclipse.core.runtime.spi.RegistryStrategy, Object, Object)}:
	 * master token and a user token. Master token allows all operations; user token
	 * allows non-persisted registry elements to be modified.
	 * @return <code>true</code> if successful, <code>false</code> if a problem was encountered
	 * @throws IllegalArgumentException if incorrect token is passed in
	 */
	public boolean addExtension(String identifier, IContributor contributor, boolean persist, String label, String extensionPointId, ConfigurationElementDescription configurationElements, Object token) throws IllegalArgumentException {
		if (!checkReadWriteAccess(token, persist))
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.addExtensionPoint() method. Check if proper access token is supplied."); //$NON-NLS-1$
		// prepare namespace information
		RegistryContributor internalContributor = (RegistryContributor) contributor;
		registryObjects.addContributor(internalContributor); // only adds a contributor if it is not already present
		String contributorId = internalContributor.getActualId();

		// addition wraps in a contribution
		Contribution contribution = getElementFactory().createContribution(contributorId, persist);
		Extension currentExtension = getElementFactory().createExtension(persist);

		String simpleId;
		String namespaceName;
		int simpleIdStart = identifier.lastIndexOf('.');
		if (simpleIdStart != -1) {
			simpleId = identifier.substring(simpleIdStart + 1);
			namespaceName = identifier.substring(0, simpleIdStart);
		} else {
			simpleId = identifier;
			namespaceName = contribution.getDefaultNamespace();
		}
		currentExtension.setSimpleIdentifier(simpleId);
		currentExtension.setNamespaceIdentifier(namespaceName);

		String extensionLabelNLS = translate(label, null);
		currentExtension.setLabel(extensionLabelNLS);

		String targetExtensionPointId;
		if (extensionPointId.indexOf('.') == -1) // No dots -> namespace name added at the start
			targetExtensionPointId = contribution.getDefaultNamespace() + '.' + extensionPointId;
		else
			targetExtensionPointId = extensionPointId;
		currentExtension.setExtensionPointIdentifier(targetExtensionPointId);

		// if we have an Id specified, check for duplicates. Only issue warning if duplicate found
		// as it might still work fine - depending on the access pattern.
		if (simpleId != null && debug()) {
			String uniqueId = namespaceName + '.' + simpleId;
			IExtension existingExtension = getExtension(uniqueId);
			if (existingExtension != null) {
				String currentSupplier = contribution.getDefaultNamespace();
				String existingSupplier = existingExtension.getContributor().getName();
				String msg = NLS.bind(RegistryMessages.parse_duplicateExtension, new String[] {currentSupplier, existingSupplier, uniqueId});
				log(new Status(IStatus.WARNING, RegistryMessages.OWNER_NAME, 0, msg, null));
				return false;
			}
		}

		getObjectManager().add(currentExtension, true);

		createExtensionData(contributorId, configurationElements, currentExtension, persist);

		currentExtension.setContributorId(contributorId);

		int[] contributionChildren = new int[3];

		contributionChildren[Contribution.EXTENSION_POINT] = 0;
		contributionChildren[Contribution.EXTENSION] = 1;
		contributionChildren[Contribution.EXTENSION + 1] = currentExtension.getObjectId();
		contribution.setRawChildren(contributionChildren);

		add(contribution);
		return true;
	}

	// Fill in the actual content of this extension
	private void createExtensionData(String contributorId, ConfigurationElementDescription description, RegistryObject parent, boolean persist) {
		ConfigurationElement currentConfigurationElement = getElementFactory().createConfigurationElement(persist);
		currentConfigurationElement.setContributorId(contributorId);
		currentConfigurationElement.setName(description.getName());

		ConfigurationElementAttribute[] descriptionProperties = description.getAttributes();

		if (descriptionProperties != null && descriptionProperties.length != 0) {
			int len = descriptionProperties.length;
			String[] properties = new String[len * 2];
			for (int i = 0; i < len; i++) {
				properties[i * 2] = descriptionProperties[i].getName();
				properties[i * 2 + 1] = translate(descriptionProperties[i].getValue(), null);
			}
			currentConfigurationElement.setProperties(properties);
		} else
			currentConfigurationElement.setProperties(RegistryObjectManager.EMPTY_STRING_ARRAY);

		String value = description.getValue();
		if (value != null)
			currentConfigurationElement.setValue(value);

		getObjectManager().add(currentConfigurationElement, true);

		// process children
		ConfigurationElementDescription[] children = description.getChildren();
		if (children != null) {
			for (ConfigurationElementDescription element : children) {
				createExtensionData(contributorId, element, currentConfigurationElement, persist);
			}
		}

		int[] oldValues = parent.getRawChildren();
		int size = oldValues.length;
		int[] newValues = new int[size + 1];
		for (int i = 0; i < size; i++) {
			newValues[i] = oldValues[i];
		}
		newValues[size] = currentConfigurationElement.getObjectId();
		parent.setRawChildren(newValues);
		currentConfigurationElement.setParentId(parent.getObjectId());
		currentConfigurationElement.setParentType(parent instanceof ConfigurationElement ? RegistryObjectManager.CONFIGURATION_ELEMENT : RegistryObjectManager.EXTENSION);
	}

	@Override
	public boolean removeExtension(IExtension extension, Object token) throws IllegalArgumentException {
		if (!(extension instanceof ExtensionHandle))
			return false;
		return removeObject(((ExtensionHandle) extension).getObject(), false, token);
	}

	@Override
	public boolean removeExtensionPoint(IExtensionPoint extensionPoint, Object token) throws IllegalArgumentException {
		if (!(extensionPoint instanceof ExtensionPointHandle))
			return false;
		return removeObject(((ExtensionPointHandle) extensionPoint).getObject(), true, token);
	}

	private boolean removeObject(RegistryObject registryObject, boolean isExtensionPoint, Object token) {
		if (!checkReadWriteAccess(token, registryObject.shouldPersist()))
			throw new IllegalArgumentException("Unauthorized access to the ExtensionRegistry.removeExtension() method. Check if proper access token is supplied."); //$NON-NLS-1$
		int id = registryObject.getObjectId();

		access.enterWrite();
		try {
			eventDelta = CombinedEventDelta.recordRemoval();
			String namespace;
			if (isExtensionPoint)
				namespace = removeExtensionPoint(id);
			else
				namespace = removeExtension(id);
			Map<Integer, RegistryObject> removed = new HashMap<>(1);
			removed.put(Integer.valueOf(id), registryObject);
			// There is some asymmetry between extension and extension point removal. Removing extension point makes
			// extensions "orphans" but does not remove them. As a result, only extensions needs to be processed.
			if (!isExtensionPoint)
				registryObjects.addAssociatedObjects(removed, registryObject);
			registryObjects.removeObjects(removed);
			registryObjects.addNavigableObjects(removed);
			IObjectManager manager = registryObjects.createDelegatingObjectManager(removed);
			getDelta(namespace).setObjectManager(manager);
			eventDelta.setObjectManager(manager);

			registryObjects.unlinkChildFromContributions(id);
			fireRegistryChangeEvent();
			eventDelta = null;
		} finally {
			access.exitWrite();
		}
		return true;
	}

	@Override
	public IContributor[] getAllContributors() {
		access.enterRead();
		try {
			return registryObjects.getContributorsSync();
		} finally {
			access.exitRead();
		}
	}

	/**
	 * <strong>EXPERIMENTAL</strong>. This method has been added as part of a work in progress.
	 * There is a guarantee neither that this API will work nor that it will remain the same.
	 * Please do not use this method without consulting with the Equinox team.
	 */
	public Object getTemporaryUserToken() {
		return userToken;
	}

	@Override
	public boolean isMultiLanguage() {
		return isMultiLanguage;
	}

	public String[] translate(String[] nonTranslated, IContributor contributor, String locale) {
		return strategy.translate(nonTranslated, contributor, locale);
	}

	public String getLocale() {
		return strategy.getLocale();
	}

	public void logMultiLangError() {
		if (mlErrorLogged) // only log this error ones
			return;
		log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, RegistryMessages.registry_non_multi_lang, new IllegalArgumentException()));
		mlErrorLogged = true;
	}
}
