/*******************************************************************************
 * Copyright (c) 2012, 2020 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.osgi.container;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.eclipse.osgi.container.Module.Settings;
import org.eclipse.osgi.container.Module.State;
import org.eclipse.osgi.container.ModuleContainerAdaptor.ContainerEvent;
import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo;
import org.eclipse.osgi.container.namespaces.EquinoxModuleDataNamespace;
import org.eclipse.osgi.framework.util.ObjectPool;
import org.eclipse.osgi.internal.container.Capabilities;
import org.eclipse.osgi.internal.container.ComputeNodeOrder;
import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Wire;
import org.osgi.service.resolver.Resolver;

/**
 * A database for storing modules, their revisions and wiring states.  The
 * database is responsible for assigning ids and providing access to the
 * capabilities provided by the revisions currently installed as well as
 * the wiring states.
 * <p>
 * <strong>Concurrent Semantics</strong><br />
 *
 * Implementations must be thread safe.  The database allows for concurrent
 * read operations and all read operations are protected by the
 * {@link #readLock() read} lock.  All write operations are
 * protected by the {@link #writeLock() write} lock.  The read and write
 * locks are reentrant and follow the semantics of the
 * {@link ReentrantReadWriteLock}.  Just like the {@code ReentrantReadWriteLock}
 * the lock on a database can not be upgraded from a read to a write.  Doing so will result in an
 * {@link IllegalMonitorStateException} being thrown.  This is behavior is different from
 * the {@code ReentrantReadWriteLock} which results in a deadlock if an attempt is made
 * to upgrade from a read to a write lock.
 * <p>
 * A database is associated with a {@link ModuleContainer container}.  The container
 * associated with a database provides public API for manipulating the modules
 * and their wiring states.  For example, installing, updating, uninstalling,
 * resolving and unresolving modules.  Except for the {@link #load(DataInputStream)},
 * all other methods that perform write operations are intended to be used by
 * the associated container.
 * @since 3.10
 */
public class ModuleDatabase {
	/**
	 * The adaptor for this database
	 */
	final ModuleContainerAdaptor adaptor;

	/**
	 * A map of modules by location.
	 */
	private final Map<String, Module> modulesByLocations;

	/**
	 * A map of modules by id.
	 */
	private final Map<Long, Module> modulesById;

	/**
	 * A map of revision wiring objects.
	 */
	final Map<ModuleRevision, ModuleWiring> wirings;

	/**
	 * Holds the next id to be assigned to a module when it is installed
	 */
	final AtomicLong nextId;

	/**
	 * Holds the current timestamp for revisions of this database.
	 */
	final AtomicLong revisionsTimeStamp;

	/**
	 * Holds the current timestamp for all changes to this database.
	 * This includes changes to revisions and changes to module settings.
	 */
	final AtomicLong allTimeStamp;

	/**
	 * Holds the construction time which is used to check for empty database on
	 * load.  This is necessary to ensure the loaded database is consistent with
	 * what was persisted.
	 */
	final long constructionTime;

	private final Capabilities capabilities;

	/**
	 * A map of module settings keyed by module id.
	 */
	final Map<Long, EnumSet<Settings>> moduleSettings;

	/**
	 * The initial module start level.
	 */
	private int initialModuleStartLevel = 1;

	/**
	 * Monitors read and write access to this database
	 */
	private final ReentrantReadWriteLock monitor = new ReentrantReadWriteLock(false);

	static enum Sort {
		BY_DEPENDENCY, BY_START_LEVEL, BY_ID;

		/**
		 * Tests if this option is contained in the specified options
		 */
		public boolean isContained(Sort... options) {
			for (Sort option : options) {
				if (equals(option)) {
					return true;
				}
			}
			return false;
		}
	}

	/**
	 * Constructs a new empty database.
	 * @param adaptor the module container adaptor
	 */
	public ModuleDatabase(ModuleContainerAdaptor adaptor) {
		this.adaptor = adaptor;
		this.modulesByLocations = new HashMap<>();
		this.modulesById = new HashMap<>();
		this.wirings = new HashMap<>();
		// Start at id 1 because 0 is reserved for the system bundle
		this.nextId = new AtomicLong(1);
		// seed with current time to avoid duplicate timestamps after using -clean
		this.constructionTime = System.currentTimeMillis();
		this.revisionsTimeStamp = new AtomicLong(constructionTime);
		this.allTimeStamp = new AtomicLong(constructionTime);
		this.moduleSettings = new HashMap<>();
		this.capabilities = new Capabilities();
	}

	/**
	 * Returns the module at the given location or null if no module exists
	 * at the given location.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @param location the location of the module.
	 * @return the module at the given location or null.
	 */
	final Module getModule(String location) {
		readLock();
		try {
			return modulesByLocations.get(location);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Returns the module at the given id or null if no module exists
	 * at the given location.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @param id the id of the module.
	 * @return the module at the given id or null.
	 */
	final Module getModule(long id) {
		readLock();
		try {
			return modulesById.get(id);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Installs a new revision using the specified builder, location and module.
	 * <p>
	 * A write operation protected by the {@link #writeLock() write} lock.
	 * @param location the location to use for the installation
	 * @param builder the builder to use to create the new revision
	 * @param revisionInfo the revision info for the new revision, may be {@code null}.
	 * @return the installed module
	 */
	final Module install(String location, ModuleRevisionBuilder builder, Object revisionInfo) {
		writeLock();
		try {
			int startlevel = Constants.SYSTEM_BUNDLE_LOCATION.equals(location) ? 0 : getInitialModuleStartLevel();
			long id = Constants.SYSTEM_BUNDLE_LOCATION.equals(location) ? 0 : builder.getId();
			if (id == -1) {
				// the id is not set by the builder; get and increment the next ID
				id = getAndIncrementNextId();
			}
			if (getModule(id) != null) {
				throw new IllegalStateException("Duplicate module id: " + id + " used by module: " + getModule(id)); //$NON-NLS-1$//$NON-NLS-2$
			}
			EnumSet<Settings> settings = getActivationPolicySettings(builder);
			Module module = load(location, builder, revisionInfo, id, settings, startlevel);
			long currentTime = System.currentTimeMillis();
			module.setlastModified(currentTime);
			setSystemLastModified(currentTime);
			incrementTimestamps(true);
			return module;
		} finally {
			writeUnlock();
		}
	}

	private EnumSet<Settings> getActivationPolicySettings(ModuleRevisionBuilder builder) {
		// do not do this for fragment bundles
		if ((builder.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) {
			return null;
		}
		for (GenericInfo info : builder.getCapabilities()) {
			if (EquinoxModuleDataNamespace.MODULE_DATA_NAMESPACE.equals(info.getNamespace())) {
				if (EquinoxModuleDataNamespace.CAPABILITY_ACTIVATION_POLICY_LAZY.equals(info.getAttributes().get(EquinoxModuleDataNamespace.CAPABILITY_ACTIVATION_POLICY))) {
					String compatibilityStartLazy = adaptor.getProperty(EquinoxConfiguration.PROP_COMPATIBILITY_START_LAZY);
					if (compatibilityStartLazy == null || Boolean.valueOf(compatibilityStartLazy)) {
						// TODO hack until p2 is fixed (bug 177641)
						EnumSet<Settings> settings = EnumSet.noneOf(Settings.class);
						settings.add(Settings.USE_ACTIVATION_POLICY);
						settings.add(Settings.AUTO_START);
						return settings;
					}
				}
				return null;
			}
		}
		return null;
	}

	final Module load(String location, ModuleRevisionBuilder builder, Object revisionInfo, long id, EnumSet<Settings> settings, int startlevel) {
		// sanity check
		checkWrite();
		if (modulesByLocations.containsKey(location))
			throw new IllegalArgumentException("Location is already used: " + location); //$NON-NLS-1$
		if (modulesById.containsKey(id))
			throw new IllegalArgumentException("Id is already used: " + id); //$NON-NLS-1$
		Module module;
		if (id == 0) {
			module = adaptor.createSystemModule();
		} else {
			module = adaptor.createModule(location, id, settings, startlevel);
		}
		builder.addRevision(module, revisionInfo);
		modulesByLocations.put(location, module);
		modulesById.put(id, module);
		if (settings != null)
			moduleSettings.put(id, settings);
		ModuleRevision newRevision = module.getCurrentRevision();
		addCapabilities(newRevision);
		return module;
	}

	/**
	 * Uninstalls the specified module from this database.
	 * Uninstalling a module will attempt to clean up any removal pending
	 * revisions possible.
	 * <p>
	 * A write operation protected by the {@link #writeLock() write} lock.
	 * @param module the module to uninstall
	 */
	final void uninstall(Module module) {
		writeLock();
		try {
			ModuleRevisions uninstalling = module.getRevisions();
			// mark the revisions as uninstalled before removing the revisions
			uninstalling.uninstall();
			// remove the location
			modulesByLocations.remove(module.getLocation());
			modulesById.remove(module.getId());
			moduleSettings.remove(module.getId());
			// remove the revisions by name
			List<ModuleRevision> revisions = uninstalling.getModuleRevisions();
			for (ModuleRevision revision : revisions) {
				// if the revision does not have a wiring it can safely be removed
				// from the revisions for the module
				ModuleWiring oldWiring = wirings.get(revision);
				if (oldWiring == null) {
					module.getRevisions().removeRevision(revision);
					removeCapabilities(revision);
				}
			}

			// attempt to cleanup any removal pendings
			cleanupRemovalPending();
			long currentTime = System.currentTimeMillis();
			module.setlastModified(currentTime);
			setSystemLastModified(currentTime);
			incrementTimestamps(true);
		} finally {
			writeUnlock();
		}
	}

	/**
	 * Updates the specified module with anew revision using the specified builder.
	 * <p>
	 * A write operation protected by the {@link #writeLock() write} lock.
	 * @param module the module for which the revision provides an update for
	 * @param builder the builder to use to create the new revision
	 * @param revisionInfo the revision info for the new revision, may be {@code null}.
	 */
	final void update(Module module, ModuleRevisionBuilder builder, Object revisionInfo) {
		writeLock();
		try {
			ModuleRevision oldRevision = module.getCurrentRevision();
			ModuleRevision newRevision = builder.addRevision(module, revisionInfo);
			addCapabilities(newRevision);

			// if the old revision does not have a wiring it can safely be removed
			ModuleWiring oldWiring = wirings.get(oldRevision);
			if (oldWiring == null) {
				module.getRevisions().removeRevision(oldRevision);
				removeCapabilities(oldRevision);
			}
			// attempt to clean up removal pendings
			cleanupRemovalPending();

			long currentTime = System.currentTimeMillis();
			module.setlastModified(currentTime);
			setSystemLastModified(currentTime);
			incrementTimestamps(true);
		} finally {
			writeUnlock();
		}
	}

	/**
	 * Examines the wirings to determine if there are any removal
	 * pending wiring objects that can be removed.  We consider
	 * a removal pending wiring as removable if all dependent
	 * wiring are also removal pending.
	 */
	void cleanupRemovalPending() {
		// sanity check
		checkWrite();
		Collection<ModuleRevision> removalPending = getRemovalPending();
		for (ModuleRevision removed : removalPending) {
			if (wirings.get(removed) == null)
				continue;
			Collection<ModuleRevision> dependencyClosure = ModuleContainer.getDependencyClosure(removed, wirings);
			boolean allPendingRemoval = true;
			for (ModuleRevision pendingRemoval : dependencyClosure) {
				if (pendingRemoval.isCurrent()) {
					allPendingRemoval = false;
					break;
				}
			}
			if (allPendingRemoval) {
				Collection<ModuleWiring> toRemoveWirings = new ArrayList<>();
				Map<ModuleWiring, Collection<ModuleWire>> toRemoveWireLists = new HashMap<>();
				for (ModuleRevision pendingRemoval : dependencyClosure) {
					ModuleWiring removedWiring = wirings.get(pendingRemoval);
					if (removedWiring == null) {
						continue;
					}
					toRemoveWirings.add(removedWiring);
					List<ModuleWire> removedWires = removedWiring.getRequiredModuleWires(null);
					for (ModuleWire wire : removedWires) {
						Collection<ModuleWire> providerWires = toRemoveWireLists.get(wire.getProviderWiring());
						if (providerWires == null) {
							providerWires = new ArrayList<>();
							toRemoveWireLists.put(wire.getProviderWiring(), providerWires);
						}
						providerWires.add(wire);
					}
				}
				for (ModuleRevision pendingRemoval : dependencyClosure) {
					pendingRemoval.getRevisions().removeRevision(pendingRemoval);
					removeCapabilities(pendingRemoval);
					wirings.remove(pendingRemoval);
				}
				// remove any wires from unresolved wirings that got removed
				for (Map.Entry<ModuleWiring, Collection<ModuleWire>> entry : toRemoveWireLists.entrySet()) {
					List<ModuleWire> provided = entry.getKey().getProvidedModuleWires(null);
					// No null checks; we are holding the write lock here.
					provided.removeAll(entry.getValue());
					entry.getKey().setProvidedWires(provided);
					for (ModuleWire removedWire : entry.getValue()) {
						// invalidate the wire
						removedWire.invalidate();
					}
				}
				// invalidate any removed wiring objects
				for (ModuleWiring moduleWiring : toRemoveWirings) {
					moduleWiring.invalidate();
				}
			}
		}
	}

	/**
	 * Gets all revisions with a removal pending wiring.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return all revisions with a removal pending wiring.
	 */
	final Collection<ModuleRevision> getRemovalPending() {
		Collection<ModuleRevision> removalPending = new ArrayList<>();
		readLock();
		try {
			for (ModuleWiring wiring : wirings.values()) {
				if (!wiring.isCurrent())
					removalPending.add(wiring.getRevision());
			}
		} finally {
			readUnlock();
		}
		return removalPending;
	}

	/**
	 * Returns the current wiring for the specified revision or
	 * null of no wiring exists for the revision.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @param revision the revision to get the wiring for
	 * @return the current wiring for the specified revision.
	 */
	final ModuleWiring getWiring(ModuleRevision revision) {
		readLock();
		try {
			return wirings.get(revision);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Returns a snapshot of the wirings for all revisions.  This
	 * performs a shallow copy of each entry in the wirings map.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return a snapshot of the wirings for all revisions.
	 */
	final Map<ModuleRevision, ModuleWiring> getWiringsCopy() {
		readLock();
		try {
			return new HashMap<>(wirings);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Returns a cloned snapshot of the wirings of all revisions.  This
	 * performs a clone of each {@link ModuleWiring}.  The
	 * {@link ModuleWiring#getRevision() revision},
	 * {@link ModuleWiring#getModuleCapabilities(String) capabilities},
	 * {@link ModuleWiring#getModuleRequirements(String) requirements},
	 * {@link ModuleWiring#getProvidedModuleWires(String) provided wires},
	 * {@link ModuleWiring#getRequiredModuleWires(String) required wires}, and
	 * {@link ModuleWiring#getSubstitutedNames()} of
	 * each wiring are copied into a cloned copy of the wiring.
	 * <p>
	 * The returned map of wirings may be safely read from while not holding
	 * any read or write locks on this database.  This is useful for doing
	 * {@link Resolver#resolve(org.osgi.service.resolver.ResolveContext) resolve}
	 * operations without holding the read or write lock on this database.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return a cloned snapshot of the wirings of all revisions.
	 */
	final Map<ModuleRevision, ModuleWiring> getWiringsClone() {
		readLock();
		try {
			Map<ModuleRevision, ModuleWiring> clonedWirings = new HashMap<>();
			for (Map.Entry<ModuleRevision, ModuleWiring> entry : wirings.entrySet()) {
				ModuleWiring wiring = new ModuleWiring(entry.getKey(), entry.getValue().getModuleCapabilities(null), entry.getValue().getModuleRequirements(null), entry.getValue().getProvidedModuleWires(null), entry.getValue().getRequiredModuleWires(null), entry.getValue().getSubstitutedNames());
				clonedWirings.put(entry.getKey(), wiring);
			}
			return clonedWirings;
		} finally {
			readUnlock();
		}
	}

	/**
	 * Replaces the complete wiring map with the specified wiring
	 * <p>
	 * A write operation protected by the {@link #writeLock() write} lock.
	 * @param newWiring the new wiring to take effect.  The values
	 * from the new wiring are copied.
	 */
	final void setWiring(Map<ModuleRevision, ModuleWiring> newWiring) {
		writeLock();
		try {
			wirings.clear();
			wirings.putAll(newWiring);
			incrementTimestamps(true);
		} finally {
			writeUnlock();
		}
	}

	/**
	 * Adds all the values from the specified delta wirings to the
	 * wirings current wirings
	 * <p>
	 * A write operation protected by the {@link #writeLock() write} lock.
	 * @param deltaWiring the new wiring values to take effect.
	 * The values from the delta wiring are copied.
	 */
	final void mergeWiring(Map<ModuleRevision, ModuleWiring> deltaWiring) {
		writeLock();
		try {
			wirings.putAll(deltaWiring);
			incrementTimestamps(true);
		} finally {
			writeUnlock();
		}
	}

	/**
	 * Returns a snapshot of all modules ordered by module ID.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return a snapshot of all modules.
	 */
	final List<Module> getModules() {
		return getSortedModules();
	}

	/**
	 * Returns a snapshot of all modules ordered according to the sort options
	 * @param sortOptions options for sorting
	 * @return a snapshot of all modules ordered according to the sort options
	 */
	final List<Module> getSortedModules(Sort... sortOptions) {
		readLock();
		try {
			List<Module> modules = new ArrayList<>(modulesByLocations.values());
			sortModules(modules, sortOptions);
			return modules;
		} finally {
			readUnlock();
		}
	}

	final void sortModules(List<Module> modules, Sort... sortOptions) {
		if (modules.size() < 2)
			return;
		if (sortOptions == null || Sort.BY_ID.isContained(sortOptions) || sortOptions.length == 0) {
			Collections.sort(modules, new Comparator<Module>() {
				@Override
				public int compare(Module m1, Module m2) {
					return m1.getId().compareTo(m2.getId());
				}
			});
			return;
		}
		// first sort by start-level
		if (Sort.BY_START_LEVEL.isContained(sortOptions)) {
			Collections.sort(modules);
		}
		if (Sort.BY_DEPENDENCY.isContained(sortOptions)) {
			if (Sort.BY_START_LEVEL.isContained(sortOptions)) {
				// sort each sublist that has modules of the same start level
				int currentSL = modules.get(0).getStartLevel();
				int currentSLindex = 0;
				boolean lazy = false;
				for (int i = 0; i < modules.size(); i++) {
					Module module = modules.get(i);
					if (currentSL != module.getStartLevel()) {
						if (lazy)
							sortByDependencies(modules.subList(currentSLindex, i));
						currentSL = module.getStartLevel();
						currentSLindex = i;
						lazy = false;
					}
					lazy |= module.isLazyActivate();
				}
				// sort the last set of bundles
				if (lazy)
					sortByDependencies(modules.subList(currentSLindex, modules.size()));
			} else {
				// sort the whole list by dependency
				sortByDependencies(modules);
			}
		}
	}

	private Collection<List<Module>> sortByDependencies(List<Module> toSort) {
		// Build references so we can sort
		List<Module[]> references = new ArrayList<>(toSort.size());
		for (Module module : toSort) {
			ModuleRevision current = module.getCurrentRevision();
			if (current == null) {
				continue;
			}
			ModuleWiring wiring = current.getWiring();
			if (wiring == null) {
				continue;
			}
			// No null check; we are holding the database lock here.
			for (ModuleWire wire : wiring.getRequiredModuleWires(null)) {
				ModuleRequirement req = wire.getRequirement();
				// Add all requirements that are not package requirements.
				// Only add package requirements that are not dynamic
				// TODO may want to consider only adding package, bundle and host requirements, other generic requirement are not that interesting
				if (!PackageNamespace.PACKAGE_NAMESPACE.equals(req.getNamespace()) || !PackageNamespace.RESOLUTION_DYNAMIC.equals(req.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE))) {
					references.add(new Module[] {wire.getRequirer().getRevisions().getModule(), wire.getProvider().getRevisions().getModule()});
				}
			}
		}

		// Sort an array using the references
		Module[] sorted = toSort.toArray(new Module[toSort.size()]);
		Object[][] cycles = ComputeNodeOrder.computeNodeOrder(sorted, references.toArray(new Module[references.size()][]));

		// Apply the sorted array to the list
		toSort.clear();
		toSort.addAll(Arrays.asList(sorted));

		if (cycles.length == 0)
			return Collections.emptyList();

		Collection<List<Module>> moduleCycles = new ArrayList<>(cycles.length);
		for (Object[] cycle : cycles) {
			List<Module> moduleCycle = new ArrayList<>(cycle.length);
			for (Object module : cycle) {
				moduleCycle.add((Module) module);
			}
			moduleCycles.add(moduleCycle);
		}
		return moduleCycles;
	}

	private void checkWrite() {
		if (monitor.getWriteHoldCount() == 0)
			throw new IllegalMonitorStateException("Must hold the write lock."); //$NON-NLS-1$
	}

	/**
	 * returns the next module ID.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return the next module ID
	 */
	public final long getNextId() {
		readLock();
		try {
			return nextId.get();
		} finally {
			readUnlock();
		}
	}

	/**
	 * Atomically increments by one the next module ID.
	 * <p>
	 * A write operation protected by the {@link #writeLock()} lock.
	 * @return the previous module ID
	 * @since 3.13
	 */
	public final long getAndIncrementNextId() {
		writeLock();
		try {
			return nextId.getAndIncrement();
		} finally {
			writeUnlock();
		}
	}

	/**
	 * Returns the current timestamp for the revisions of this database.
	 * The timestamp is incremented any time a modification
	 * is made to the revisions in this database.  For example:
	 * <ul>
	 *   <li> installing a module
	 *   <li> updating a module
	 *   <li> uninstalling a module
	 *   <li> modifying the wirings
	 * </ul>
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return the current timestamp of this database.
	 */
	final public long getRevisionsTimestamp() {
		readLock();
		try {
			return revisionsTimeStamp.get();
		} finally {
			readUnlock();
		}
	}

	/**
	 * Returns the current timestamp for  this database.
	 * The timestamp is incremented any time a modification
	 * is made to this database.  This includes the modifications
	 * described in {@link #getRevisionsTimestamp() revisions timestamp}
	 * and the following modifications related to modules:
	 * <ul>
	 *   <li> modifying the initial module start level
	 *   <li> modifying a module start level
	 *   <li> modifying a module settings
	 * </ul>
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * @return the current timestamp of this database.
	 */
	final public long getTimestamp() {
		readLock();
		try {
			return allTimeStamp.get();
		} finally {
			readUnlock();
		}
	}

	/**
	 * Increments the timestamps of this database.
	 * @param incrementRevision indicates if the revision timestamp should change
	 */
	private void incrementTimestamps(boolean incrementRevision) {
		// sanity check
		checkWrite();
		if (incrementRevision) {
			revisionsTimeStamp.incrementAndGet();
		}
		allTimeStamp.incrementAndGet();
		adaptor.updatedDatabase();
	}

	private void setSystemLastModified(long currentTime) {
		// sanity check
		checkWrite();
		Module systemModule = getModule(0);
		if (systemModule != null) {
			systemModule.setlastModified(currentTime);
		}
	}

	/**
	 * Acquires the read lock for this database.
	 * @see ReadLock#lock()
	 */
	public final void readLock() {
		monitor.readLock().lock();
	}

	/**
	 * Acquires the write lock for this database.
	 * Same as {@link WriteLock#lock()} except an illegal
	 * state exception is thrown if the current thread holds
	 * one or more read locks.
	 * @see WriteLock#lock()
	 * @throws IllegalMonitorStateException if the current thread holds
	 * one or more read locks.
	 */
	public final void writeLock() {
		if (monitor.getReadHoldCount() > 0) {
			// this is not supported and will cause deadlock if allowed to proceed.
			// fail fast instead of deadlocking
			throw new IllegalMonitorStateException("Requesting upgrade to write lock."); //$NON-NLS-1$
		}
		monitor.writeLock().lock();
	}

	/**
	 * Attempts to release the read lock for this database.
	 * @see ReadLock#unlock()
	 */
	public final void readUnlock() {
		monitor.readLock().unlock();
	}

	/**
	 * Attempts to release the write lock for this database.
	 * @see WriteLock#unlock()
	 */
	public final void writeUnlock() {
		monitor.writeLock().unlock();
	}

	/**
	 * Adds the {@link ModuleRevision#getModuleCapabilities(String) capabilities}
	 * provided by the specified revision to this database.  These capabilities must
	 * become available for lookup with the {@link ModuleDatabase#findCapabilities(Requirement)}
	 * method.
	 * <p>
	 * This method must be called while holding the {@link #writeLock() write} lock.
	 * @param revision the revision which has capabilities to add
	 */
	final void addCapabilities(ModuleRevision revision) {
		checkWrite();
		Collection<String> packageNames = capabilities.addCapabilities(revision);
		// Clear the dynamic miss caches for all the package names added
		for (ModuleWiring wiring : wirings.values()) {
			wiring.removeDynamicPackageMisses(packageNames);
		}
	}

	/**
	 * Removes the {@link ModuleRevision#getModuleCapabilities(String) capabilities}
	 * provided by the specified revision from this database.  These capabilities
	 * must no longer be available for lookup with the
	 * {@link ModuleDatabase#findCapabilities(Requirement)} method.
	 * <p>
	 * This method must be called while holding the {@link #writeLock() write} lock.
	 * @param revision
	 */
	protected void removeCapabilities(ModuleRevision revision) {
		checkWrite();
		capabilities.removeCapabilities(revision);
	}

	/**
	 * Returns a mutable snapshot of capabilities that are candidates for
	 * satisfying the specified requirement.
	 * <p>
	 * A read operation protected by the {@link #readLock() read} lock.
	 * Implementers of this method should acquire the read lock while
	 * finding capabilities.
	 * @param requirement the requirement
	 * @return the candidates for the requirement
	 */
	final List<ModuleCapability> findCapabilities(Requirement requirement) {
		readLock();
		try {
			return capabilities.findCapabilities(requirement);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Writes this database in a format suitable for using the {@link #load(DataInputStream)}
	 * method.  All modules are stored which have a current {@link ModuleRevision revision}.
	 * Only the current revision of each module is stored (no removal pending revisions
	 * are stored).  Optionally the {@link ModuleWiring wiring} of each current revision
	 * may be stored.  Wiring can only be stored if there are no {@link #getRemovalPending()
	 * removal pending} revisions.
	 * <p>
	 * This method acquires the {@link #readLock() read} lock while writing this
	 * database.
	 * <p>
	 * After this database have been written, the output stream is flushed.
	 * The output stream remains open after this method returns.
	 * @param out the data output steam.
	 * @param persistWirings true if wirings should be persisted.  This option will be ignored
	 *        if there are {@link #getRemovalPending() removal pending} revisions.
	 * @throws IOException if writing this database to the specified output stream throws an IOException
	 */
	public final void store(DataOutputStream out, boolean persistWirings) throws IOException {
		readLock();
		try {
			Persistence.store(this, out, persistWirings);
		} finally {
			readUnlock();
		}
	}

	/**
	 * Loads information into this database from the input data stream.  This data
	 * base must be empty and never been modified (the {@link #getRevisionsTimestamp() timestamp} is zero).
	 * All stored modules are loaded into this database.  If the input stream contains
	 * wiring then it will also be loaded into this database.
	 * <p>
	 * Since this method modifies this database it is considered a write operation.
	 * This method acquires the {@link #writeLock() write} lock while loading
	 * the information into this database.
	 * <p>
	 * The specified stream remains open after this method returns.
	 * @param in the data input stream.
	 * @throws IOException if an error occurred when reading from the input stream.
	 * @throws IllegalStateException if this database is not empty.
	 */
	public final void load(DataInputStream in) throws IOException {
		writeLock();
		try {
			if (allTimeStamp.get() != constructionTime)
				throw new IllegalStateException("Can only load into a empty database."); //$NON-NLS-1$
			Persistence.load(this, in);
		} finally {
			writeUnlock();
		}
	}

	final void persistSettings(EnumSet<Settings> settings, Module module) {
		writeLock();
		try {
			EnumSet<Settings> existing = moduleSettings.get(module.getId());
			if (!settings.equals(existing)) {
				moduleSettings.put(module.getId(), EnumSet.copyOf(settings));
				incrementTimestamps(false);
			}
		} finally {
			writeUnlock();
		}
	}

	final void setStartLevel(Module module, int startlevel) {
		writeLock();
		try {
			module.checkValid();
			module.storeStartLevel(startlevel);
			incrementTimestamps(false);
		} finally {
			writeUnlock();
		}
	}

	final int getInitialModuleStartLevel() {
		readLock();
		try {
			return this.initialModuleStartLevel;
		} finally {
			readUnlock();
		}
	}

	final void setInitialModuleStartLevel(int initialStartlevel) {
		writeLock();
		try {
			this.initialModuleStartLevel = initialStartlevel;
			incrementTimestamps(false);
		} finally {
			writeUnlock();
		}
	}

	private static class Persistence {
		private static final int VERSION = 3;
		private static final byte NULL = 0;
		private static final byte OBJECT = 1;
		private static final byte INDEX = 2;
		private static final byte LONG_STRING = 3;

		private static final byte VALUE_STRING = 0;
		// REMOVED treated as List<String> - private static final byte VALUE_STRING_ARRAY = 1;
		// REMOVED never was really supported by the OSGi builder - private static final byte VAlUE_BOOLEAN = 2;
		// REMOVED never was really supported by the OSGi builder - private static final byte VALUE_INTEGER = 3;
		private static final byte VALUE_LONG = 4;
		private static final byte VALUE_DOUBLE = 5;
		private static final byte VALUE_VERSION = 6;
		// REMOVED treated as type String - private static final byte VALUE_URI = 7;
		private static final byte VALUE_LIST = 8;

		private static int addToWriteTable(Object object, Map<Object, Integer> objectTable) {
			if (object == null)
				throw new NullPointerException();
			Integer cur = objectTable.get(object);
			if (cur != null)
				throw new IllegalStateException("Object is already in the write table: " + object); //$NON-NLS-1$
			objectTable.put(object, Integer.valueOf(objectTable.size()));
			// return the index of the object just added (i.e. size - 1)
			return (objectTable.size() - 1);
		}

		private static void addToReadTable(Object object, int index, List<Object> objectTable) {
			if (index == objectTable.size()) {
				objectTable.add(object);
			} else if (index < objectTable.size()) {
				objectTable.set(index, object);
			} else {
				while (objectTable.size() < index) {
					objectTable.add(null);
				}
				objectTable.add(object);
			}
		}

		public static void store(ModuleDatabase moduleDatabase, DataOutputStream out, boolean persistWirings) throws IOException {
			out.writeInt(VERSION);
			out.writeLong(moduleDatabase.getRevisionsTimestamp());
			out.writeLong(moduleDatabase.getTimestamp());
			out.writeLong(moduleDatabase.getNextId());
			out.writeInt(moduleDatabase.getInitialModuleStartLevel());

			// prime the object table with all the strings, versions and maps
			Set<String> allStrings = new HashSet<>();
			Set<Version> allVersions = new HashSet<>();
			Set<Map<String, ?>> allMaps = new HashSet<>();

			// first gather all the strings, versions and maps from the modules
			List<Module> modules = moduleDatabase.getModules();
			for (Module module : modules) {
				getStringsVersionsAndMaps(module, moduleDatabase, allStrings, allVersions, allMaps);
			}
			// outside of the modules the wirings have 'substituted' packages strings
			Map<ModuleRevision, ModuleWiring> wirings = moduleDatabase.wirings;
			for (ModuleWiring wiring : wirings.values()) {
				Collection<String> substituted = wiring.getSubstitutedNames();
				allStrings.addAll(substituted);
			}

			// Now persist all the Strings
			Map<Object, Integer> objectTable = new HashMap<>();
			allStrings.remove(null);
			out.writeInt(allStrings.size());
			for (String string : allStrings) {
				writeString(string, out, objectTable);
				out.writeInt(addToWriteTable(string, objectTable));
			}
			// Followed by versions which may reference strings with their qualifier
			out.writeInt(allVersions.size());
			for (Version version : allVersions) {
				writeVersion(version, out, objectTable);
				out.writeInt(addToWriteTable(version, objectTable));
			}
			// Followed by maps which may reference the strings and versions
			out.writeInt(allMaps.size());
			for (Map<String, ?> map : allMaps) {
				writeMap(map, out, objectTable, moduleDatabase);
				out.writeInt(addToWriteTable(map, objectTable));
			}

			// Followed by modules which reference the strings, versions, and maps
			out.writeInt(modules.size());
			for (Module module : modules) {
				writeModule(module, moduleDatabase, out, objectTable);
			}

			Collection<ModuleRevision> removalPendings = moduleDatabase.getRemovalPending();
			// only persist wirings if there are no removals pending
			persistWirings &= removalPendings.isEmpty();
			out.writeBoolean(persistWirings);
			if (!persistWirings) {
				return;
			}

			// prime the object table with all the required wires which reference the modules
			out.writeInt(wirings.size());
			for (ModuleWiring wiring : wirings.values()) {
				List<ModuleWire> requiredWires = wiring.getPersistentRequiredWires();
				out.writeInt(requiredWires.size());
				for (ModuleWire wire : requiredWires) {
					writeWire(wire, out, objectTable);
				}
			}

			// now write all the info about each wiring using only indexes from the objectTable
			for (ModuleWiring wiring : wirings.values()) {
				writeWiring(wiring, out, objectTable);
			}

			out.flush();
		}

		private static void getStringsVersionsAndMaps(Module module, ModuleDatabase moduleDatabase, Set<String> allStrings, Set<Version> allVersions, Set<Map<String, ?>> allMaps) {
			ModuleRevision current = module.getCurrentRevision();
			if (current == null)
				return;

			allStrings.add(module.getLocation());
			allStrings.add(current.getSymbolicName());
			allStrings.add(current.getVersion().getQualifier());
			allVersions.add(current.getVersion());
			EnumSet<Settings> settings = moduleDatabase.moduleSettings.get(module.getId());
			if (settings != null) {
				for (Settings setting : settings) {
					allStrings.add(setting.toString());
				}
			}

			List<ModuleCapability> capabilities = current.getModuleCapabilities(null);
			for (ModuleCapability capability : capabilities) {
				allStrings.add(capability.getNamespace());
				addMap(capability.getPersistentAttributes(), allStrings, allVersions, allMaps);
				addMap(capability.getDirectives(), allStrings, allVersions, allMaps);
			}

			List<ModuleRequirement> requirements = current.getModuleRequirements(null);
			for (ModuleRequirement requirement : requirements) {
				allStrings.add(requirement.getNamespace());
				addMap(requirement.getAttributes(), allStrings, allVersions, allMaps);
				addMap(requirement.getDirectives(), allStrings, allVersions, allMaps);
			}
		}

		private static void addMap(Map<String, ?> map, Set<String> allStrings, Set<Version> allVersions, Set<Map<String, ?>> allMaps) {
			if (!allMaps.add(map)) {
				// map was already added
				return;
			}
			for (Map.Entry<String, ?> entry : map.entrySet()) {
				allStrings.add(entry.getKey());
				Object value = entry.getValue();
				if (value instanceof String) {
					allStrings.add((String) value);
				} else if (value instanceof Version) {
					allStrings.add(((Version) value).getQualifier());
					allVersions.add((Version) value);
				} else if (value instanceof List) {
					switch (getListType((List<?>) value)) {
						case VALUE_STRING :
							for (Object string : (List<?>) value) {
								allStrings.add((String) string);
							}
							break;
						case VALUE_VERSION :
							for (Object version : (List<?>) value) {
								allStrings.add(((Version) version).getQualifier());
								allVersions.add((Version) version);
							}
							break;
					}
				}
			}
		}

		public static void load(ModuleDatabase moduleDatabase, DataInputStream in) throws IOException {
			int version = in.readInt();
			if (version > VERSION || VERSION / 1000 != version / 1000)
				throw new IllegalArgumentException("The version of the persistent framework data is not compatible: " + version + " expecting: " + VERSION); //$NON-NLS-1$ //$NON-NLS-2$
			long revisionsTimeStamp = in.readLong();
			long allTimeStamp = in.readLong();
			moduleDatabase.nextId.set(in.readLong());
			moduleDatabase.setInitialModuleStartLevel(in.readInt());

			List<Object> objectTable = new ArrayList<>();

			if (version >= 2) {
				int numStrings = in.readInt();
				for (int i = 0; i < numStrings; i++) {
					readIndexedString(in, objectTable);
				}
				int numVersions = in.readInt();
				for (int i = 0; i < numVersions; i++) {
					readIndexedVersion(in, objectTable);
				}
				int numMaps = in.readInt();
				for (int i = 0; i < numMaps; i++) {
					readIndexedMap(in, objectTable);
				}
			}
			int numModules = in.readInt();
			ModuleRevisionBuilder builder = new ModuleRevisionBuilder();
			for (int i = 0; i < numModules; i++) {
				readModule(builder, moduleDatabase, in, objectTable, version);
			}

			moduleDatabase.revisionsTimeStamp.set(revisionsTimeStamp);
			moduleDatabase.allTimeStamp.set(allTimeStamp);
			if (!in.readBoolean())
				return; // no wires persisted

			int numWirings = in.readInt();
			// prime the table with all the required wires
			for (int i = 0; i < numWirings; i++) {
				int numWires = in.readInt();
				for (int j = 0; j < numWires; j++) {
					readWire(in, objectTable);
				}
			}

			// now read all the info about each wiring using only indexes
			Map<ModuleRevision, ModuleWiring> wirings = new HashMap<>();
			for (int i = 0; i < numWirings; i++) {
				ModuleWiring wiring = readWiring(in, objectTable);
				wirings.put(wiring.getRevision(), wiring);
			}
			// TODO need to do this without incrementing the timestamp
			moduleDatabase.setWiring(wirings);

			// need to set the resolution state of the modules
			for (ModuleWiring wiring : wirings.values()) {
				wiring.getRevision().getRevisions().getModule().setState(State.RESOLVED);
			}

			// Setting the timestamp at the end since some operations increment it
			moduleDatabase.revisionsTimeStamp.set(revisionsTimeStamp);
			moduleDatabase.allTimeStamp.set(allTimeStamp);
		}

		private static void writeModule(Module module, ModuleDatabase moduleDatabase, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			ModuleRevision current = module.getCurrentRevision();
			if (current == null)
				return;
			out.writeInt(addToWriteTable(current, objectTable));

			writeString(module.getLocation(), out, objectTable);
			out.writeLong(module.getId());

			writeString(current.getSymbolicName(), out, objectTable);
			writeVersion(current.getVersion(), out, objectTable);
			out.writeInt(current.getTypes());

			List<ModuleCapability> capabilities = current.getModuleCapabilities(null);
			out.writeInt(capabilities.size());
			for (ModuleCapability capability : capabilities) {
				out.writeInt(addToWriteTable(capability, objectTable));
				writeGenericInfo(capability.getNamespace(), capability.getPersistentAttributes(), capability.getDirectives(), out, objectTable);
			}

			List<Requirement> requirements = current.getRequirements(null);
			out.writeInt(requirements.size());
			for (Requirement requirement : requirements) {
				out.writeInt(addToWriteTable(requirement, objectTable));
				writeGenericInfo(requirement.getNamespace(), requirement.getAttributes(), requirement.getDirectives(), out, objectTable);
			}

			// settings
			EnumSet<Settings> settings = moduleDatabase.moduleSettings.get(module.getId());
			out.writeInt(settings == null ? 0 : settings.size());
			if (settings != null) {
				for (Settings setting : settings) {
					writeString(setting.name(), out, objectTable);
				}
			}

			// startlevel
			out.writeInt(module.getStartLevel());

			// last modified
			out.writeLong(module.getLastModified());
		}

		private static void readModule(ModuleRevisionBuilder builder, ModuleDatabase moduleDatabase, DataInputStream in, List<Object> objectTable, int version) throws IOException {
			builder.clear();
			int moduleIndex = in.readInt();
			String location = readString(in, objectTable);
			long id = in.readLong();
			builder.setSymbolicName(readString(in, objectTable));
			builder.setVersion(readVersion(in, objectTable));
			builder.setTypes(in.readInt());

			int numCapabilities = in.readInt();
			int[] capabilityIndexes = new int[numCapabilities];
			for (int i = 0; i < numCapabilities; i++) {
				capabilityIndexes[i] = in.readInt();
				readGenericInfo(true, in, builder, objectTable, version);
			}

			int numRequirements = in.readInt();
			int[] requirementIndexes = new int[numRequirements];
			for (int i = 0; i < numRequirements; i++) {
				requirementIndexes[i] = in.readInt();
				readGenericInfo(false, in, builder, objectTable, version);
			}

			// settings
			EnumSet<Settings> settings = null;
			int numSettings = in.readInt();
			if (numSettings > 0) {
				settings = EnumSet.noneOf(Settings.class);
				for (int i = 0; i < numSettings; i++) {
					settings.add(Settings.valueOf(readString(in, objectTable)));
				}
			}

			// startlevel
			int startlevel = in.readInt();
			Object revisionInfo = moduleDatabase.adaptor.getRevisionInfo(location, id);
			Module module = moduleDatabase.load(location, builder, revisionInfo, id, settings, startlevel);

			// last modified
			module.setlastModified(in.readLong());

			ModuleRevision current = module.getCurrentRevision();
			addToReadTable(current, moduleIndex, objectTable);

			List<ModuleCapability> capabilities = current.getModuleCapabilities(null);
			for (int i = 0; i < capabilities.size(); i++) {
				addToReadTable(capabilities.get(i), capabilityIndexes[i], objectTable);
			}

			List<ModuleRequirement> requirements = current.getModuleRequirements(null);
			for (int i = 0; i < requirements.size(); i++) {
				addToReadTable(requirements.get(i), requirementIndexes[i], objectTable);
			}
		}

		private static void writeWire(ModuleWire wire, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			Wire w = wire;
			Integer capability = objectTable.get(w.getCapability());
			Integer provider = objectTable.get(w.getProvider());
			Integer requirement = objectTable.get(w.getRequirement());
			Integer requirer = objectTable.get(w.getRequirer());

			if (capability == null || provider == null || requirement == null || requirer == null)
				throw new NullPointerException("Could not find the expected indexes"); //$NON-NLS-1$

			out.writeInt(addToWriteTable(wire, objectTable));

			out.writeInt(capability);
			out.writeInt(provider);
			out.writeInt(requirement);
			out.writeInt(requirer);
		}

		private static void readWire(DataInputStream in, List<Object> objectTable) throws IOException {
			int wireIndex = in.readInt();

			ModuleCapability capability = (ModuleCapability) objectTable.get(in.readInt());
			ModuleRevision provider = (ModuleRevision) objectTable.get(in.readInt());
			ModuleRequirement requirement = (ModuleRequirement) objectTable.get(in.readInt());
			ModuleRevision requirer = (ModuleRevision) objectTable.get(in.readInt());

			if (capability == null || provider == null || requirement == null || requirer == null)
				throw new NullPointerException("Could not find the expected indexes"); //$NON-NLS-1$

			ModuleWire result = new ModuleWire(capability, provider, requirement, requirer);

			addToReadTable(result, wireIndex, objectTable);
		}

		private static void writeWiring(ModuleWiring wiring, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			Integer revisionIndex = objectTable.get(wiring.getRevision());
			if (revisionIndex == null)
				throw new NullPointerException("Could not find revision for wiring."); //$NON-NLS-1$
			out.writeInt(revisionIndex);

			List<ModuleCapability> capabilities = wiring.getModuleCapabilities(null);
			out.writeInt(capabilities.size());
			for (ModuleCapability capability : capabilities) {
				Integer capabilityIndex = objectTable.get(capability);
				if (capabilityIndex == null)
					throw new NullPointerException("Could not find capability for wiring."); //$NON-NLS-1$
				out.writeInt(capabilityIndex);
			}

			List<ModuleRequirement> requirements = wiring.getPersistentRequirements();
			out.writeInt(requirements.size());
			for (ModuleRequirement requirement : requirements) {
				Integer requirementIndex = objectTable.get(requirement);
				if (requirementIndex == null)
					throw new NullPointerException("Could not find requirement for wiring."); //$NON-NLS-1$
				out.writeInt(requirementIndex);
			}

			List<ModuleWire> providedWires = wiring.getPersistentProvidedWires();
			out.writeInt(providedWires.size());
			for (ModuleWire wire : providedWires) {
				Integer wireIndex = objectTable.get(wire);
				if (wireIndex == null)
					throw new NullPointerException("Could not find provided wire for wiring."); //$NON-NLS-1$
				out.writeInt(wireIndex);
			}

			List<ModuleWire> requiredWires = wiring.getPersistentRequiredWires();
			out.writeInt(requiredWires.size());
			for (ModuleWire wire : requiredWires) {
				Integer wireIndex = objectTable.get(wire);
				if (wireIndex == null)
					throw new NullPointerException("Could not find required wire for wiring."); //$NON-NLS-1$
				out.writeInt(wireIndex);
			}

			Collection<String> substituted = wiring.getSubstitutedNames();
			out.writeInt(substituted.size());
			for (String pkgName : substituted) {
				writeString(pkgName, out, objectTable);
			}
		}

		private static ModuleWiring readWiring(DataInputStream in, List<Object> objectTable) throws IOException {
			ModuleRevision revision = (ModuleRevision) objectTable.get(in.readInt());
			if (revision == null)
				throw new NullPointerException("Could not find revision for wiring."); //$NON-NLS-1$

			int numCapabilities = in.readInt();
			List<ModuleCapability> capabilities = new ArrayList<>(numCapabilities);
			for (int i = 0; i < numCapabilities; i++) {
				capabilities.add((ModuleCapability) objectTable.get(in.readInt()));
			}

			int numRequirements = in.readInt();
			List<ModuleRequirement> requirements = new ArrayList<>(numRequirements);
			for (int i = 0; i < numRequirements; i++) {
				requirements.add((ModuleRequirement) objectTable.get(in.readInt()));
			}

			int numProvidedWires = in.readInt();
			List<ModuleWire> providedWires = new ArrayList<>(numProvidedWires);
			for (int i = 0; i < numProvidedWires; i++) {
				providedWires.add((ModuleWire) objectTable.get(in.readInt()));
			}

			int numRequiredWires = in.readInt();
			List<ModuleWire> requiredWires = new ArrayList<>(numRequiredWires);
			for (int i = 0; i < numRequiredWires; i++) {
				requiredWires.add((ModuleWire) objectTable.get(in.readInt()));
			}

			int numSubstitutedNames = in.readInt();
			Collection<String> substituted = new ArrayList<>(numSubstitutedNames);
			for (int i = 0; i < numSubstitutedNames; i++) {
				substituted.add(readString(in, objectTable));
			}

			return new ModuleWiring(revision, capabilities, requirements, providedWires, requiredWires, substituted);
		}

		private static void writeGenericInfo(String namespace, Map<String, ?> attributes, Map<String, String> directives, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			writeString(namespace, out, objectTable);

			Integer attributesIndex = objectTable.get(attributes);
			Integer directivesIndex = objectTable.get(directives);
			if (attributesIndex == null || directivesIndex == null)
				throw new NullPointerException("Could not find the expected indexes"); //$NON-NLS-1$
			out.writeInt(attributesIndex);
			out.writeInt(directivesIndex);
		}

		@SuppressWarnings("unchecked")
		private static void readGenericInfo(boolean isCapability, DataInputStream in, ModuleRevisionBuilder builder, List<Object> objectTable, int version) throws IOException {
			String namespace = readString(in, objectTable);
			Map<String, Object> attributes = version >= 2 ? (Map<String, Object>) objectTable.get(in.readInt()) : readMap(in, objectTable);
			Map<String, ?> directives = version >= 2 ? (Map<String, ?>) objectTable.get(in.readInt()) : readMap(in, objectTable);
			if (attributes == null || directives == null)
				throw new NullPointerException("Could not find the expected indexes"); //$NON-NLS-1$
			if (isCapability) {
				builder.basicAddCapability(namespace, (Map<String, String>) directives, attributes);
			} else {
				builder.basicAddRequirement(namespace, (Map<String, String>) directives, attributes);
			}

		}

		private static void writeMap(Map<String, ?> source, DataOutputStream out, Map<Object, Integer> objectTable, ModuleDatabase moduleDatabase) throws IOException {
			if (source == null) {
				out.writeInt(0);
			} else {
				out.writeInt(source.size());
				Iterator<String> iter = source.keySet().iterator();
				while (iter.hasNext()) {
					String key = iter.next();
					Object value = source.get(key);
					writeString(key, out, objectTable);
					if (value instanceof String) {
						out.writeByte(VALUE_STRING);
						writeString((String) value, out, objectTable);
					} else if (value instanceof Long) {
						out.writeByte(VALUE_LONG);
						out.writeLong(((Long) value).longValue());
					} else if (value instanceof Double) {
						out.writeByte(VALUE_DOUBLE);
						out.writeDouble(((Double) value).doubleValue());
					} else if (value instanceof Version) {
						out.writeByte(VALUE_VERSION);
						writeVersion((Version) value, out, objectTable);
					} else if (value instanceof List) {
						out.writeByte(VALUE_LIST);
						writeList(out, key, (List<?>) value, objectTable, moduleDatabase);
					} else {
						// do our best and write a string; post an error.
						// This will be difficult to debug because we don't know which module it is coming from, but it is better than being silent
						moduleDatabase.adaptor.publishContainerEvent(ContainerEvent.ERROR, moduleDatabase.getModule(0), new BundleException("Invalid map value: " + key + " = " + value.getClass().getName() + '[' + value + ']')); //$NON-NLS-1$ //$NON-NLS-2$
						out.writeByte(VALUE_STRING);
						writeString(String.valueOf(value), out, objectTable);
					}
				}
			}
		}

		private static void readIndexedMap(DataInputStream in, List<Object> objectTable) throws IOException {
			Map<String, Object> result = readMap(in, objectTable);
			addToReadTable(result, in.readInt(), objectTable);
		}

		private static Map<String, Object> readMap(DataInputStream in, List<Object> objectTable) throws IOException {
			int count = in.readInt();
			Map<String, Object> result;
			if (count == 0) {
				result = Collections.emptyMap();
			} else if (count == 1) {
				String key = readString(in, objectTable);
				byte type = in.readByte();
				Object value = readMapValue(in, type, objectTable);
				result = Collections.singletonMap(key, value);
			} else {
				result = new HashMap<>(count);
				for (int i = 0; i < count; i++) {
					String key = readString(in, objectTable);
					byte type = in.readByte();
					Object value = readMapValue(in, type, objectTable);
					result.put(key, value);
				}
				result = Collections.unmodifiableMap(result);
			}
			return result;
		}

		private static Object readMapValue(DataInputStream in, int type, List<Object> objectTable) throws IOException {
			switch (type) {
				case VALUE_STRING :
					return readString(in, objectTable);
				case VALUE_LONG :
					return Long.valueOf(in.readLong());
				case VALUE_DOUBLE :
					return Double.valueOf(in.readDouble());
				case VALUE_VERSION :
					return readVersion(in, objectTable);
				case VALUE_LIST :
					return readList(in, objectTable);
				default :
					throw new IllegalArgumentException("Invalid type: " + type); //$NON-NLS-1$
			}
		}

		private static void writeList(DataOutputStream out, String key, List<?> list, Map<Object, Integer> objectTable, ModuleDatabase moduleDatabase) throws IOException {
			if (list.isEmpty()) {
				out.writeInt(0);
				return;
			}
			byte type = getListType(list);
			if (type == -1) {
				out.writeInt(0);
				return; // don't understand the list type
			}
			out.writeInt(list.size());
			out.writeByte(type == -2 ? VALUE_STRING : type);
			for (Object value : list) {
				switch (type) {
					case VALUE_STRING :
						writeString((String) value, out, objectTable);
						break;
					case VALUE_LONG :
						out.writeLong(((Long) value).longValue());
						break;
					case VALUE_DOUBLE :
						out.writeDouble(((Double) value).doubleValue());
						break;
					case VALUE_VERSION :
						writeVersion((Version) value, out, objectTable);
						break;
					default :
						// do our best and write a string; post an error.
						// This will be difficult to debug because we don't know which module it is coming from, but it is better than being silent
						moduleDatabase.adaptor.publishContainerEvent(ContainerEvent.ERROR, moduleDatabase.getModule(0), new BundleException("Invalid list element in map: " + key + " = " + value.getClass().getName() + '[' + value + ']')); //$NON-NLS-1$ //$NON-NLS-2$
						writeString(String.valueOf(value), out, objectTable);
						break;
				}
			}
		}

		private static byte getListType(List<?> list) {
			if (list.size() == 0)
				return -1;
			Object type = list.get(0);
			if (type instanceof String)
				return VALUE_STRING;
			if (type instanceof Long)
				return VALUE_LONG;
			if (type instanceof Double)
				return VALUE_DOUBLE;
			if (type instanceof Version)
				return VALUE_VERSION;
			return -2;
		}

		private static List<?> readList(DataInputStream in, List<Object> objectTable) throws IOException {
			int size = in.readInt();
			if (size == 0)
				return Collections.emptyList();
			byte listType = in.readByte();
			if (size == 1) {
				return Collections.singletonList(readListValue(listType, in, objectTable));
			}
			List<Object> list = new ArrayList<>(size);
			for (int i = 0; i < size; i++) {
				list.add(readListValue(listType, in, objectTable));
			}
			return Collections.unmodifiableList(list);
		}

		private static Object readListValue(byte listType, DataInputStream in, List<Object> objectTable) throws IOException {
			switch (listType) {
				case VALUE_STRING :
					return readString(in, objectTable);
				case VALUE_LONG :
					return Long.valueOf(in.readLong());
				case VALUE_DOUBLE :
					return Double.valueOf(in.readDouble());
				case VALUE_VERSION :
					return readVersion(in, objectTable);
				default :
					throw new IllegalArgumentException("Invalid type: " + listType); //$NON-NLS-1$
			}
		}

		private static void writeVersion(Version version, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			if (version == null || version.equals(Version.emptyVersion)) {
				out.writeByte(NULL);
				return;
			}
			Integer index = objectTable.get(version);
			if (index != null) {
				out.writeByte(INDEX);
				out.writeInt(index);
				return;
			}
			out.writeByte(OBJECT);
			out.writeInt(version.getMajor());
			out.writeInt(version.getMinor());
			out.writeInt(version.getMicro());
			writeQualifier(version.getQualifier(), out, objectTable);
		}

		private static void writeQualifier(String string, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			if (string != null && string.length() == 0)
				string = null;
			writeString(string, out, objectTable);
		}

		private static Version readIndexedVersion(DataInputStream in, List<Object> objectTable) throws IOException {
			Version version = readVersion0(in, objectTable, false);
			addToReadTable(version, in.readInt(), objectTable);
			return version;
		}

		private static Version readVersion(DataInputStream in, List<Object> objectTable) throws IOException {
			return readVersion0(in, objectTable, true);
		}

		private static Version readVersion0(DataInputStream in, List<Object> objectTable, boolean intern) throws IOException {
			byte type = in.readByte();
			if (type == INDEX) {
				int index = in.readInt();
				return (Version) objectTable.get(index);
			}
			if (type == NULL)
				return Version.emptyVersion;
			int majorComponent = in.readInt();
			int minorComponent = in.readInt();
			int serviceComponent = in.readInt();
			String qualifierComponent = readString(in, objectTable);
			Version version = new Version(majorComponent, minorComponent, serviceComponent, qualifierComponent);
			return intern ? ObjectPool.intern(version) : version;
		}

		private static void writeString(String string, DataOutputStream out, Map<Object, Integer> objectTable) throws IOException {
			Integer index = string != null ? objectTable.get(string) : null;
			if (index != null) {
				out.writeByte(INDEX);
				out.writeInt(index);
				return;
			}

			if (string == null)
				out.writeByte(NULL);
			else {
				byte[] data = string.getBytes(StandardCharsets.UTF_8);

				if (data.length > 65535) {
					out.writeByte(LONG_STRING);
					out.writeInt(data.length);
					out.write(data);
				} else {
					out.writeByte(OBJECT);
					out.writeUTF(string);
				}
			}
		}

		static private String readIndexedString(DataInputStream in, List<Object> objectTable) throws IOException {
			String string = readString0(in, objectTable, false);
			addToReadTable(string, in.readInt(), objectTable);
			return string;
		}

		static private String readString(DataInputStream in, List<Object> objectTable) throws IOException {
			return readString0(in, objectTable, true);
		}

		static private String readString0(DataInputStream in, List<Object> objectTable, boolean intern) throws IOException {
			byte type = in.readByte();
			if (type == INDEX) {
				int index = in.readInt();
				return (String) objectTable.get(index);
			}
			if (type == NULL) {
				return null;
			}
			String string;
			if (type == LONG_STRING) {
				int length = in.readInt();
				byte[] data = new byte[length];
				in.readFully(data);
				string = new String(data, StandardCharsets.UTF_8);
			} else {
				string = in.readUTF();
			}

			return intern ? ObjectPool.intern(string) : string;
		}
	}
}
