/*******************************************************************************
 * Copyright (c) 2005, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 
 *******************************************************************************/
package org.eclipse.dltk.internal.core;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ISavedState;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.dltk.compiler.problem.IProblem;
import org.eclipse.dltk.compiler.problem.IProblemReporter;
import org.eclipse.dltk.compiler.util.HashtableOfObjectToInt;
import org.eclipse.dltk.core.BuildpathContainerInitializer;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.DLTKLanguageManager;
import org.eclipse.dltk.core.IAccessRule;
import org.eclipse.dltk.core.IBuildpathAttribute;
import org.eclipse.dltk.core.IBuildpathContainer;
import org.eclipse.dltk.core.IBuildpathEntry;
import org.eclipse.dltk.core.IBuiltinModuleProvider;
import org.eclipse.dltk.core.IDLTKLanguageToolkit;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IModelStatus;
import org.eclipse.dltk.core.IParent;
import org.eclipse.dltk.core.IProblemRequestor;
import org.eclipse.dltk.core.IProjectFragment;
import org.eclipse.dltk.core.IScriptFolder;
import org.eclipse.dltk.core.IScriptModel;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceModule;
import org.eclipse.dltk.core.ISourceModuleInfoCache;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.WorkingCopyOwner;
import org.eclipse.dltk.core.search.indexing.IndexManager;
import org.eclipse.dltk.internal.core.builder.ScriptBuilder;
import org.eclipse.dltk.internal.core.search.DLTKWorkspaceScope;
import org.eclipse.dltk.internal.core.util.Messages;
import org.eclipse.dltk.internal.core.util.Util;
import org.eclipse.dltk.internal.core.util.WeakHashSet;
import org.osgi.service.prefs.BackingStoreException;

public class ModelManager implements ISaveParticipant {
	private final static int CONTAINERS_FILE_VERSION = 1;
	public final static String BP_CONTAINER_PREFERENCES_PREFIX = DLTKCore.PLUGIN_ID
			+ ".buildpathContainer."; //$NON-NLS-1$
	public final static String BP_USERLIBRARY_PREFERENCES_PREFIX = DLTKCore.PLUGIN_ID
			+ ".userLibrary."; //$NON-NLS-1$
	public final static String BP_ENTRY_IGNORE = "##<cp entry ignore>##"; //$NON-NLS-1$
	public final static IPath BP_ENTRY_IGNORE_PATH = new Path(BP_ENTRY_IGNORE);
	/**
	 * Unique handle onto the Model
	 */
	final Model model = new Model();
	/**
	 * Buildpath containers pool
	 */
	public HashMap containers = new HashMap(5);
	public HashMap previousSessionContainers = new HashMap(5);
	private ThreadLocal containerInitializationInProgress = new ThreadLocal();
	public boolean batchContainerInitializations = false;
	public HashMap containerInitializersCache = new HashMap(5);
	public final static IBuildpathContainer CONTAINER_INITIALIZATION_IN_PROGRESS = new IBuildpathContainer() {
		public IBuildpathEntry[] getBuildpathEntries(IScriptProject project) {
			return null;
		}

		public String getDescription(IScriptProject project) {
			return "Container Initialization In Progress";} //$NON-NLS-1$

		public int getKind() {
			return 0;
		}

		public IPath getPath() {
			return null;
		}

		public String toString() {
			return getDescription(null);
		}

		public IBuiltinModuleProvider getBuiltinProvider(IScriptProject project) {
			return null;
		}
	};
	/*
	 * A HashSet that contains the IScriptProject whose buildpath is being
	 * resolved.
	 */
	private ThreadLocal buildpathsBeingResolved = new ThreadLocal();

	public static class PerProjectInfo {
		public IProject project;
		public Object savedState;
		public boolean triedRead;
		public IBuildpathEntry[] rawBuildpath;
		public IModelStatus rawBuildpathStatus;
		public IBuildpathEntry[] resolvedBuildpath;
		public IModelStatus unresolvedEntryStatus;
		public Map resolvedPathToRawEntries; // reverse map from resolved
		// path to raw entries
		public IEclipsePreferences preferences;
		public Hashtable options;

		public PerProjectInfo(IProject project) {
			this.triedRead = false;
			this.savedState = null;
			this.project = project;
		}

		public void rememberExternalLibTimestamps() {
			// TODO remember external lib timestamps
		}

		// updating raw buildpath need to flush obsoleted cached information
		// about resolved entries
		public synchronized void updateBuildpathInformation(
				IBuildpathEntry[] newRawBuildpath) {
			this.rawBuildpath = newRawBuildpath;
			this.resolvedBuildpath = null;
			this.resolvedPathToRawEntries = null;
		}

		public String toString() {
			StringBuffer buffer = new StringBuffer();
			buffer.append("Info for "); //$NON-NLS-1$
			buffer.append(this.project.getFullPath());
			buffer.append("\nRaw buildpath:\n"); //$NON-NLS-1$
			if (this.rawBuildpath == null) {
				buffer.append("  <null>\n"); //$NON-NLS-1$
			} else {
				for (int i = 0, length = this.rawBuildpath.length; i < length; i++) {
					buffer.append("  "); //$NON-NLS-1$
					buffer.append(this.rawBuildpath[i]);
					buffer.append('\n');
				}
			}
			buffer.append("Resolved buildpath:\n"); //$NON-NLS-1$
			IBuildpathEntry[] resolvedCP = this.resolvedBuildpath;
			if (resolvedCP == null) {
				buffer.append("  <null>\n"); //$NON-NLS-1$
			} else {
				for (int i = 0, length = resolvedCP.length; i < length; i++) {
					buffer.append("  "); //$NON-NLS-1$
					buffer.append(resolvedCP[i]);
					buffer.append('\n');
				}
			}
			return buffer.toString();
		}

	}

	public static class PerWorkingCopyInfo implements IProblemRequestor {
		int useCount = 0;
		IProblemRequestor problemRequestor;
		IProblemReporter problemReporter;
		ISourceModule workingCopy;
		boolean noProblemReporter;

		public PerWorkingCopyInfo(ISourceModule workingCopy,
				IProblemRequestor problemRequestor) {
			this.workingCopy = workingCopy;
			this.problemRequestor = problemRequestor;
		}

		public PerWorkingCopyInfo(ISourceModule workingCopy,
				IProblemRequestor problemRequestor,
				IProblemReporter problemReporter) {
			this(workingCopy, problemRequestor);
			this.problemReporter = problemReporter;
		}

		public void acceptProblem(IProblem problem) {
			if (this.problemRequestor == null)
				return;
			this.problemRequestor.acceptProblem(problem);
		}

		public void beginReporting() {
			if (this.problemRequestor == null)
				return;
			this.problemRequestor.beginReporting();
		}

		public void endReporting() {
			if (this.problemRequestor == null)
				return;
			this.problemRequestor.endReporting();
		}

		public ISourceModule getWorkingCopy() {
			return this.workingCopy;
		}

		public boolean isActive() {
			return this.problemRequestor != null
					&& this.problemRequestor.isActive();
		}

		public String toString() {
			StringBuffer buffer = new StringBuffer();
			buffer.append("Info for "); //$NON-NLS-1$
			buffer.append(((ModelElement) this.workingCopy)
					.toStringWithAncestors());
			buffer.append("\nUse count = "); //$NON-NLS-1$
			buffer.append(this.useCount);
			buffer.append("\nProblem requestor:\n  "); //$NON-NLS-1$
			buffer.append(this.problemRequestor);
			return buffer.toString();
		}
	}

	public static boolean VERBOSE = DLTKCore.VERBOSE_MODEL_MANAGER;
	public static boolean BP_RESOLVE_VERBOSE = DLTKCore.VERBOSE_BP_RESOLVE;
	/**
	 * Name of the extension point for contributing buildpath container
	 * initializers
	 */
	public static final String BPCONTAINER_INITIALIZER_EXTPOINT_ID = "buildpathContainerInitializer"; //$NON-NLS-1$
	public static boolean ZIP_ACCESS_VERBOSE = DLTKCore.VERBOSE_ZIP_ACCESS;
	/**
	 * A cache of opened zip files per thread. (for a given thread, the object
	 * value is a HashMap from IPath to java.io.ZipFile)
	 */
	private ThreadLocal zipFiles = new ThreadLocal();

	private UserLibraryManager userLibraryManager;

	public final static ISourceModule[] NO_WORKING_COPY = new ISourceModule[0];
	/**
	 * The singleton manager
	 */
	private static ModelManager MANAGER = new ModelManager();
	/**
	 * Infos cache.
	 */
	public ModelCache cache;// = new ModelCache();
	/*
	 * Temporary cache of newly opened elements
	 */
	private ThreadLocal temporaryCache = new ThreadLocal();
	/**
	 * Set of elements which are out of sync with their buffers.
	 */
	protected HashSet elementsOutOfSynchWithBuffers = new HashSet(11);
	/**
	 * Holds the state used for delta processing.
	 */
	public DeltaProcessingState deltaState = new DeltaProcessingState();
	public IndexManager indexManager = new IndexManager();
	/**
	 * Table from IProject to PerProjectInfo. NOTE: this object itself is used
	 * as a lock to synchronize creation/removal of per project infos
	 */
	protected Map perProjectInfos = new HashMap(5);
	/**
	 * Table from WorkingCopyOwner to a table of ISourceModule (working copy
	 * handle) to PerWorkingCopyInfo. NOTE: this object itself is used as a lock
	 * to synchronize creation/removal of per working copy infos
	 */
	protected Map perWorkingCopyInfos = new HashMap(5);
	public final IEclipsePreferences[] preferencesLookup = new IEclipsePreferences[2];
	static final int PREF_INSTANCE = 0;
	static final int PREF_DEFAULT = 1;
	// Preferences
	HashSet optionNames = new HashSet(20);
	Hashtable optionsCache;
	/*
	 * Pools of symbols used in the model. Used as a replacement for
	 * String#intern() that could prevent garbage collection of strings on some
	 * Interpreters.
	 */
	private WeakHashSet stringSymbols = new WeakHashSet(5);
	Map workspaceScope = null;
	public static final String DELTA_LISTENER_PERF = DLTKCore.PLUGIN_ID
			+ "/perf/deltalistener"; //$NON-NLS-1$

	/**
	 * Constructs a new ModelManager
	 */
	private ModelManager() {
		// singleton: prevent others from creating a new instance
		if (Platform.isRunning())
			this.indexManager = new IndexManager();
	}

	/**
	 * Returns the handle to the active script model.
	 */
	public final Model getModel() {
		return this.model;
	}

	/**
	 * Returns the singleton ModelManager
	 */
	public final static ModelManager getModelManager() {
		return MANAGER;
	}

	/**
	 * Returns the set of elements which are out of synch with their buffers.
	 */
	protected HashSet getElementsOutOfSynchWithBuffers() {
		return this.elementsOutOfSynchWithBuffers;
	}

	/**
	 * Returns the info for the element.
	 */
	public synchronized Object getInfo(IModelElement element) {
		HashMap tempCache = (HashMap) this.temporaryCache.get();
		if (tempCache != null) {
			Object result = tempCache.get(element);
			if (result != null) {
				return result;
			}
		}
		return this.cache.getInfo(element);
	}

	/**
	 * Returns the info for this element without disturbing the cache ordering.
	 */
	protected synchronized Object peekAtInfo(IModelElement element) {
		HashMap tempCache = (HashMap) this.temporaryCache.get();
		if (tempCache != null) {
			Object result = tempCache.get(element);
			if (result != null) {
				return result;
			}
		}
		return this.cache.peekAtInfo(element);
	}

	/*
	 * Removes all cached info for the given element (including all children)
	 * from the cache. Returns the info for the given element, or null if it was
	 * closed.
	 */
	public synchronized Object removeInfoAndChildren(ModelElement element)
			throws ModelException {
		Object info = this.cache.peekAtInfo(element);
		if (info != null) {
			boolean wasVerbose = false;
			try {
				if (VERBOSE) {
					String elementType;
					switch (element.getElementType()) {
					case IModelElement.SCRIPT_PROJECT:
						elementType = "project"; //$NON-NLS-1$
						break;
					case IModelElement.PROJECT_FRAGMENT:
						elementType = "root"; //$NON-NLS-1$
						break;
					case IModelElement.SCRIPT_FOLDER:
						elementType = "folder"; //$NON-NLS-1$
						break;
					case IModelElement.BINARY_MODULE:
						elementType = "binary module"; //$NON-NLS-1$
						break;
					case IModelElement.SOURCE_MODULE:
						elementType = "source module"; //$NON-NLS-1$
						break;
					default:
						elementType = "element"; //$NON-NLS-1$
					}
					System.out
							.println(Thread.currentThread()
									+ " CLOSING " + elementType + " " + element.toStringWithAncestors()); //$NON-NLS-1$//$NON-NLS-2$
					wasVerbose = true;
					VERBOSE = false;
				}
				element.closing(info);
				if (element instanceof IParent
						&& info instanceof ModelElementInfo) {
					IModelElement[] children = ((ModelElementInfo) info)
							.getChildren();
					for (int i = 0, size = children.length; i < size; ++i) {
						ModelElement child = (ModelElement) children[i];
						child.close();
					}
				}
				this.cache.removeInfo(element);
				if (wasVerbose) {
					System.out.println(this.cache.toStringFillingRation("-> ")); //$NON-NLS-1$
				}
			} finally {
				ModelManager.VERBOSE = wasVerbose;
			}
			return info;
		}
		return null;
	}

	/*
	 * Puts the infos in the given map (keys are IModelElements and values are
	 * ModelElementInfos) in the model cache in an atomic way. First checks that
	 * the info for the opened element (or one of its ancestors) has not been
	 * added to the cache. If it is the case, another thread has opened the
	 * element (or one of its ancestors). So returns without updating the cache.
	 */
	protected synchronized void putInfos(IModelElement openedElement,
			Map newElements) {
		// remove children
		Object existingInfo = this.cache.peekAtInfo(openedElement);
		if (openedElement instanceof IParent
				&& existingInfo instanceof ModelElementInfo) {
			IModelElement[] children = ((ModelElementInfo) existingInfo)
					.getChildren();
			for (int i = 0, size = children.length; i < size; ++i) {
				ModelElement child = (ModelElement) children[i];
				try {
					child.close();
				} catch (ModelException e) {
					// ignore
				}
			}
		}
		// Need to put any ArchiveProjectFragment in first.
		// This is due to the way the LRU cache flushes entries.
		// When a BinaryFolder is flused from the LRU cache, the entire
		// archive is flushed by removing the ArchiveProjectFragment and all of
		// its
		// children (see ElementCache.close()). If we flush the BinaryFolder
		// when its ArchiveProjectFragment is not in the cache and the root is
		// about to be
		// added (during the 'while' loop), we will end up in an inconsist
		// state.
		// Subsequent resolution against package in the archive would fail as a
		// result.
		for (Iterator it = newElements.entrySet().iterator(); it.hasNext();) {
			Map.Entry entry = (Map.Entry) it.next();
			IModelElement element = (IModelElement) entry.getKey();
			if (element instanceof ArchiveProjectFragment
					|| element instanceof ExternalScriptFolder) {
				Object info = entry.getValue();
				it.remove();
				this.cache.putInfo(element, info);
			}
		}
		Iterator iterator = newElements.keySet().iterator();
		while (iterator.hasNext()) {
			IModelElement element = (IModelElement) iterator.next();
			Object info = newElements.get(element);
			this.cache.putInfo(element, info);
		}
	}

	/**
	 * Returns the temporary cache for newly opened elements for the current
	 * thread. Creates it if not already created.
	 */
	public HashMap getTemporaryCache() {
		HashMap result = (HashMap) this.temporaryCache.get();
		if (result == null) {
			result = new HashMap();
			this.temporaryCache.set(result);
		}
		return result;
	}

	/*
	 * Resets the temporary cache for newly created elements to null.
	 */
	public void resetTemporaryCache() {
		this.temporaryCache.set(null);
	}

	/*
	 * Returns whether there is a temporary cache for the current thread.
	 */
	public boolean hasTemporaryCache() {
		return this.temporaryCache.get() != null;
	}

	/**
	 * Returns the model element corresponding to the given file, its project
	 * being the given project. Returns <code>null</code> if unable to associate
	 * the given file with a model element.
	 * 
	 * Creating a model element has the side effect of creating and opening all
	 * of the element's parents if they are not yet open.
	 */
	public static IModelElement create(IFile file, IScriptProject project) {
		if (file == null) {
			return null;
		}
		if (project == null) {
			project = DLTKCore.create(file.getProject());
		}
		// FIXME at the moment we can create source modules only
		if (Util.isValidSourceModule(project, file)) {
			return createSourceModuleFrom(file, project);
		}
		return null;
	}

	/**
	 * Creating an element has the side effect of creating and opening all of
	 * the element's parents if they are not yet open.
	 */
	public static IModelElement create(IResource resource,
			IScriptProject project) {
		if (resource == null) {
			return null;
		}
		int type = resource.getType();
		switch (type) {
		case IResource.PROJECT:
			return DLTKCore.create((IProject) resource);
		case IResource.FILE:
			return create((IFile) resource, project);
		case IResource.FOLDER:
			return create((IFolder) resource, project);
		case IResource.ROOT:
			return DLTKCore.create((IWorkspaceRoot) resource);
		default:
			return null;
		}
	}

	public static IModelElement create(IFolder folder, IScriptProject project) {
		if (folder == null) {
			return null;
		}
		IModelElement element;
		if (project == null) {
			project = DLTKCore.create(folder.getProject());
			element = determineIfOnBuildpath(folder, project);
			if (element == null) {
				// walk all projects and find one that have the given folder on
				// its buildpath
				IScriptProject[] projects;
				try {
					projects = ModelManager.getModelManager().getModel()
							.getScriptProjects();
				} catch (ModelException e) {
					return null;
				}
				for (int i = 0, length = projects.length; i < length; i++) {
					project = projects[i];
					element = determineIfOnBuildpath(folder, project);
					if (element != null)
						break;
				}
			}
		} else {
			element = determineIfOnBuildpath(folder, project);
		}
		return element;
	}

	/**
	 * Creates and returns a source module element for the given file, its
	 * project being the given project. Returns <code>null</code> if unable to
	 * recognize the source module.
	 */
	public static ISourceModule createSourceModuleFrom(IFile file,
			IScriptProject project) {
		if (file == null)
			return null;
		if (project == null) {
			project = DLTKCore.create(file.getProject());
		}
		IScriptFolder folder = (IScriptFolder) determineIfOnBuildpath(file,
				project);
		if (folder == null) {
			// not on buildpath - make the root its folder
			IProjectFragment root = project
					.getProjectFragment(file.getParent());
			folder = root.getScriptFolder(Path.EMPTY);
			if (VERBOSE) {
				System.out
						.println("WARNING : creating module element outside buildpath (" + Thread.currentThread() + "): " + file.getFullPath()); //$NON-NLS-1$//$NON-NLS-2$
			}
		}
		return folder.getSourceModule(file.getName());
	}

	/**
	 * Returns the project fragment root represented by the resource, or the
	 * folder the given resource is located in, or <code>null</code> if the
	 * given resource is not on the buildpath of the given project.
	 */
	public static IModelElement determineIfOnBuildpath(IResource resource,
			IScriptProject project) {
		IPath resourcePath = resource.getFullPath();
		try {
			IBuildpathEntry[] entries = ((ScriptProject) project)
					.getResolvedBuildpath();
			for (int i = 0; i < entries.length; i++) {
				IBuildpathEntry entry = entries[i];
				if (entry.getEntryKind() == IBuildpathEntry.BPE_PROJECT)
					continue;
				IPath rootPath = entry.getPath();
				if (rootPath.equals(resourcePath)) {
					return project.getProjectFragment(resource);
				} else if (rootPath.isPrefixOf(resourcePath)) {
					// given we have a resource child of the root, it cannot
					// be
					// a ZIP fragment
					ProjectFragment root = (ProjectFragment) ((ScriptProject) project)
							.getFolderProjectFragment(rootPath);
					if (root == null)
						return null;
					IPath folderPath = resourcePath
							.removeFirstSegments(rootPath.segmentCount());
					if (resource.getType() == IResource.FILE) {
						// if the resource is a file, then remove the last
						// segment which
						// is the file name in the folder
						folderPath = folderPath.removeLastSegments(1);
					}
					return root.getScriptFolder(folderPath);
				}
			}
		} catch (ModelException npe) {
			return null;
		}
		return null;
	}

	/*
	 * Returns the per-project info for the given project. If specified, create
	 * the info if the info doesn't exist.
	 */
	public PerProjectInfo getPerProjectInfo(IProject project, boolean create) {
		synchronized (this.perProjectInfos) { // use the perProjectInfo
			// collection as its own lock
			PerProjectInfo info = (PerProjectInfo) this.perProjectInfos
					.get(project);
			if (info == null && create) {
				info = new PerProjectInfo(project);
				this.perProjectInfos.put(project, info);
			}
			return info;
		}
	}

	/*
	 * Returns the per-project info for the given project. If the info doesn't
	 * exist, check for the project existence and create the info. @throws
	 * ModelException if the project doesn't exist.
	 */
	public PerProjectInfo getPerProjectInfoCheckExistence(IProject project)
			throws ModelException {
		ModelManager.PerProjectInfo info = getPerProjectInfo(project, false /*
																			 * don't
																			 * create
																			 * info
																			 */);
		if (info == null) {
			info = getPerProjectInfo(project, true /* create info */);
		}
		return info;
	}

	/*
	 * Returns the per-working copy info for the given working copy at the given
	 * path. If it doesn't exist and if create, add a new per-working copy info
	 * with the given problem requestor. If recordUsage, increment the
	 * per-working copy info's use count. Returns null if it doesn't exist and
	 * not create.
	 */
	public PerWorkingCopyInfo getPerWorkingCopyInfo(SourceModule workingCopy,
			boolean create, boolean recordUsage,
			IProblemRequestor problemRequestor) {
		return getPerWorkingCopyInfo(workingCopy, create, recordUsage,
				problemRequestor, null);
	}

	public PerWorkingCopyInfo getPerWorkingCopyInfo(SourceModule workingCopy,
			boolean create, boolean recordUsage,
			IProblemRequestor problemRequestor, IProblemReporter problemReporter) {
		synchronized (this.perWorkingCopyInfos) { // use the
			// perWorkingCopyInfo
			// collection as its own
			// lock
			WorkingCopyOwner owner = workingCopy.getOwner();
			Map workingCopyToInfos = (Map) this.perWorkingCopyInfos.get(owner);
			if (workingCopyToInfos == null && create) {
				workingCopyToInfos = new HashMap();
				this.perWorkingCopyInfos.put(owner, workingCopyToInfos);
			}
			PerWorkingCopyInfo info = workingCopyToInfos == null ? null
					: (PerWorkingCopyInfo) workingCopyToInfos.get(workingCopy);
			if (info == null && create) {
				info = new PerWorkingCopyInfo(workingCopy, problemRequestor,
						problemReporter);
				workingCopyToInfos.put(workingCopy, info);
			}
			if (info != null && recordUsage)
				info.useCount++;
			return info;
		}
	}

	/*
	 * Returns all the working copies which have the given owner. Adds the
	 * working copies of the primary owner if specified. Returns null if it has
	 * none.
	 */
	public ISourceModule[] getWorkingCopies(WorkingCopyOwner owner,
			boolean addPrimary) {
		synchronized (this.perWorkingCopyInfos) {
			ISourceModule[] primaryWCs = addPrimary
					&& owner != DefaultWorkingCopyOwner.PRIMARY ? getWorkingCopies(
					DefaultWorkingCopyOwner.PRIMARY, false)
					: null;
			Map workingCopyToInfos = (Map) this.perWorkingCopyInfos.get(owner);
			if (workingCopyToInfos == null)
				return primaryWCs;
			int primaryLength = primaryWCs == null ? 0 : primaryWCs.length;
			int size = workingCopyToInfos.size(); // note size is > 0
			// otherwise
			// pathToPerWorkingCopyInfos
			// would be null
			ISourceModule[] result = new ISourceModule[primaryLength + size];
			int index = 0;
			if (primaryWCs != null) {
				for (int i = 0; i < primaryLength; i++) {
					ISourceModule primaryWorkingCopy = primaryWCs[i];
					boolean validSrcModule = false;
					IResource res = primaryWorkingCopy.getResource();
					if (res != null) {
						validSrcModule = Util.isValidSourceModule(
								primaryWorkingCopy, res);
					} else {
						validSrcModule = Util.isValidSourceModule(
								primaryWorkingCopy, primaryWorkingCopy
										.getPath());
					}
					if (validSrcModule) {
						ISourceModule workingCopy = new SourceModule(
								(ScriptFolder) primaryWorkingCopy.getParent(),
								primaryWorkingCopy.getElementName(), owner);
						if (!workingCopyToInfos.containsKey(workingCopy))
							result[index++] = primaryWorkingCopy;
					} else {
						System.err.println("Not valid primary working copy:" //$NON-NLS-1$
								+ primaryWorkingCopy.getElementName());
					}
				}
				if (index != primaryLength)
					System.arraycopy(result, 0,
							result = new ISourceModule[index + size], 0, index);
			}
			Iterator iterator = workingCopyToInfos.values().iterator();
			while (iterator.hasNext()) {
				result[index++] = ((ModelManager.PerWorkingCopyInfo) iterator
						.next()).getWorkingCopy();
			}
			return result;
		}
	}

	public DeltaProcessor getDeltaProcessor() {
		return this.deltaState.getDeltaProcessor();
	}

	public IndexManager getIndexManager() {
		return this.indexManager;
	}

	public Object getLastBuiltState(IProject project, IProgressMonitor monitor) {
		if (!DLTKLanguageManager.hasScriptNature(project)) {
			if (ScriptBuilder.DEBUG)
				System.out.println(project + " is not a Java project"); //$NON-NLS-1$
			return null; // should never be requested on non-Java projects
		}
		PerProjectInfo info = getPerProjectInfo(project, true/*
															 * create if missing
															 */);
		if (!info.triedRead) {
			info.triedRead = true;
			try {
				if (monitor != null)
					monitor.subTask(Messages
							.bind(Messages.build_readStateProgress, project
									.getName()));
				info.savedState = readState(project);
			} catch (CoreException e) {
				e.printStackTrace();
			}
		}
		return info.savedState;
	}

	public String getOption(String optionName) {
		if (DLTKCore.CORE_ENCODING.equals(optionName)) {
			return DLTKCore.getEncoding();
		}
		String propertyName = optionName;
		if (this.optionNames.contains(propertyName)) {
			IPreferencesService service = Platform.getPreferencesService();
			String value = service
					.get(optionName, null, this.preferencesLookup);
			return value == null ? null : value.trim();
		}
		return null;
	}

	/**
	 * Get workpsace eclipse preference for ScriptCore plugin.
	 */
	public IEclipsePreferences getInstancePreferences() {
		return preferencesLookup[PREF_INSTANCE];
	}

	public void setOptions(Hashtable newOptions) {
		try {
			IEclipsePreferences defaultPreferences = getDefaultPreferences();
			IEclipsePreferences instancePreferences = getInstancePreferences();
			if (newOptions == null) {
				instancePreferences.clear();
			} else {
				Enumeration keys = newOptions.keys();
				while (keys.hasMoreElements()) {
					String key = (String) keys.nextElement();
					if (!this.optionNames.contains(key))
						continue; // unrecognized option
					if (key.equals(DLTKCore.CORE_ENCODING))
						continue; // skipped, contributed by resource prefs
					String value = (String) newOptions.get(key);
					String defaultValue = defaultPreferences.get(key, null);
					if (defaultValue != null && defaultValue.equals(value)) {
						instancePreferences.remove(key);
					} else {
						instancePreferences.put(key, value);
					}
				}
			}
			// persist options
			instancePreferences.flush();
			// update cache
			this.optionsCache = newOptions == null ? null : new Hashtable(
					newOptions);
		} catch (BackingStoreException e) {
			// ignore
		}
	}

	public Hashtable getOptions() {
		// return cached options if already computed
		if (this.optionsCache != null)
			return new Hashtable(this.optionsCache);
		if (!Platform.isRunning()) {
			return this.optionsCache = getDefaultOptionsNoInitialization();
		}
		// init
		Hashtable options = new Hashtable(10);
		IPreferencesService service = Platform.getPreferencesService();
		// set options using preferences service lookup
		Iterator iterator = optionNames.iterator();
		while (iterator.hasNext()) {
			String propertyName = (String) iterator.next();
			String propertyValue = service.get(propertyName, null,
					this.preferencesLookup);
			if (propertyValue != null) {
				options.put(propertyName, propertyValue);
			}
		}
		// get encoding through resource plugin
		options.put(DLTKCore.CORE_ENCODING, DLTKCore.getEncoding());
		// store built map in cache
		this.optionsCache = new Hashtable(options);
		// return built map
		return options;
	}

	/*
	 * Reset project options stored in info cache.
	 */
	public void resetProjectOptions(ScriptProject scriptProject) {
		synchronized (this.perProjectInfos) { // use the perProjectInfo
			// collection as its own lock
			IProject project = scriptProject.getProject();
			PerProjectInfo info = (PerProjectInfo) this.perProjectInfos
					.get(project);
			if (info != null) {
				info.options = null;
			}
		}
	}

	/*
	 * Reset project preferences stored in info cache.
	 */
	public void resetProjectPreferences(ScriptProject scriptProject) {
		synchronized (this.perProjectInfos) { // use the perProjectInfo
			// collection as its own lock
			IProject project = scriptProject.getProject();
			PerProjectInfo info = (PerProjectInfo) this.perProjectInfos
					.get(project);
			if (info != null) {
				info.preferences = null;
			}
		}
	}

	public void setBuildpathBeingResolved(IScriptProject project,
			boolean buildpathIsResolved) {
		if (buildpathIsResolved) {
			getBuildpathBeingResolved().add(project);
		} else {
			getBuildpathBeingResolved().remove(project);
		}
	}

	private HashSet getBuildpathBeingResolved() {
		HashSet result = (HashSet) this.buildpathsBeingResolved.get();
		if (result == null) {
			result = new HashSet();
			this.buildpathsBeingResolved.set(result);
		}
		return result;
	}

	public boolean isBuildpathBeingResolved(IScriptProject project) {
		return getBuildpathBeingResolved().contains(project);
	}

	/*
	 * Discards the per working copy info for the given working copy (making it
	 * a compilation unit) if its use count was 1. Otherwise, just decrement the
	 * use count. If the working copy is primary, computes the delta between its
	 * state and the original compilation unit and register it. Close the
	 * working copy, its buffer and remove it from the shared working copy
	 * table. Ignore if no per-working copy info existed. NOTE: it must NOT be
	 * synchronized as it may interact with the element info cache (if useCount
	 * is decremented to 0), see bug 50667. Returns the new use count (or -1 if
	 * it didn't exist).
	 */
	public int discardPerWorkingCopyInfo(SourceModule workingCopy)
			throws ModelException {
		// create the delta builder (this remembers the current content of the
		// working copy)
		// outside the perWorkingCopyInfos lock (see bug 50667)
		ModelElementDeltaBuilder deltaBuilder = null;
		if (workingCopy.isPrimary() && workingCopy.hasUnsavedChanges()) {
			deltaBuilder = new ModelElementDeltaBuilder(workingCopy);
		}
		PerWorkingCopyInfo info = null;
		synchronized (this.perWorkingCopyInfos) {
			WorkingCopyOwner owner = workingCopy.getOwner();
			Map workingCopyToInfos = (Map) this.perWorkingCopyInfos.get(owner);
			if (workingCopyToInfos == null)
				return -1;
			info = (PerWorkingCopyInfo) workingCopyToInfos.get(workingCopy);
			if (info == null)
				return -1;
			if (--info.useCount == 0) {
				// remove per working copy info
				workingCopyToInfos.remove(workingCopy);
				if (workingCopyToInfos.isEmpty()) {
					this.perWorkingCopyInfos.remove(owner);
				}
			}
		}
		if (info.useCount == 0) { // info cannot be null here (check was done
			// above)
			// remove infos + close buffer (since no longer working copy)
			// outside the perWorkingCopyInfos lock (see bug 50667)
			removeInfoAndChildren(workingCopy);
			workingCopy.closeBuffer();
			// compute the delta if needed and register it if there are changes
			if (deltaBuilder != null) {
				deltaBuilder.buildDeltas();
				if ((deltaBuilder.delta != null)
						&& (deltaBuilder.delta.getAffectedChildren().length > 0)) {
					getDeltaProcessor().registerModelDelta(deltaBuilder.delta);
				}
			}
		}
		return info.useCount;
	}

	public synchronized String intern(String s) {
		// make sure to copy the string (so that it doesn't hold on the
		// underlying char[] that might be much bigger than necessary)
		return (String) this.stringSymbols.add(s);
	}

	public void startup() throws CoreException {
		try {
			// initialize Model model cache
			this.cache = new ModelCache();
			// request state folder creation (workaround 19885)
			DLTKCore.getPlugin().getStateLocation();
			// Initialize eclipse preferences
			initializePreferences();
			// Listen to preference changes
			Preferences.IPropertyChangeListener propertyListener = new Preferences.IPropertyChangeListener() {
				public void propertyChange(Preferences.PropertyChangeEvent event) {
					ModelManager.this.optionsCache = null;
				}
			};
			DLTKCore.getPlugin().getPluginPreferences()
					.addPropertyChangeListener(propertyListener);
			long start = -1;
			if (VERBOSE)
				start = System.currentTimeMillis();
			loadContainers();
			if (VERBOSE)
				traceContainers("Loaded", start); //$NON-NLS-1$
			final IWorkspace workspace = ResourcesPlugin.getWorkspace();
			workspace.addResourceChangeListener(this.deltaState,
					IResourceChangeEvent.PRE_BUILD
							| IResourceChangeEvent.POST_BUILD
							| IResourceChangeEvent.POST_CHANGE
							| IResourceChangeEvent.PRE_DELETE
							| IResourceChangeEvent.PRE_CLOSE);
			startIndexing();
			// process deltas since last activated in indexer thread so that
			// indexes are up-to-date.
			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38658
			Job processSavedState = new Job(Messages.savedState_jobName) {
				protected IStatus run(IProgressMonitor monitor) {
					try {
						// add save participant and process delta atomically
						// see
						// https://bugs.eclipse.org/bugs/show_bug.cgi?id=59937
						workspace.run(new IWorkspaceRunnable() {
							public void run(IProgressMonitor progress)
									throws CoreException {
								ISavedState savedState = workspace
										.addSaveParticipant(DLTKCore
												.getPlugin(), ModelManager.this);
								if (savedState != null) {
									// the event type coming from the saved
									// state is always POST_AUTO_BUILD
									// force it to be POST_CHANGE so that the
									// delta processor can handle it
									ModelManager.this.deltaState
											.getDeltaProcessor().overridenEventType = IResourceChangeEvent.POST_CHANGE;
									savedState
											.processResourceChangeEvents(ModelManager.this.deltaState);
								}
							}
						}, monitor);
					} catch (CoreException e) {
						return e.getStatus();
					}
					return Status.OK_STATUS;
				}
			};
			processSavedState.setSystem(true);
			processSavedState.setPriority(Job.SHORT); // process asap
			processSavedState.schedule();
		} catch (RuntimeException e) {
			shutdown();
			throw e;
		}
	}

	private void startIndexing() {
		getIndexManager().reset();
	}

	/**
	 * Update the buildpath variable cache
	 */
	public static class EclipsePreferencesListener implements
			IEclipsePreferences.IPreferenceChangeListener {
		/**
		 * @see org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener#preferenceChange(org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent)
		 */
		public void preferenceChange(
				IEclipsePreferences.PreferenceChangeEvent event) {
			String propertyName = event.getKey();
			if (propertyName.startsWith(BP_CONTAINER_PREFERENCES_PREFIX)) {
				recreatePersistedContainer(propertyName, (String) event
						.getNewValue(), false);
			} else if (propertyName
					.startsWith(BP_USERLIBRARY_PREFERENCES_PREFIX)) {
				String libName = propertyName
						.substring(BP_USERLIBRARY_PREFERENCES_PREFIX.length());
				UserLibraryManager manager = ModelManager
						.getUserLibraryManager();
				manager
						.updateUserLibrary(libName, (String) event
								.getNewValue());
			}
		}
	}

	/**
	 * Reads the build state for the relevant project.
	 */
	protected Object readState(IProject project) throws CoreException {
		File file = getSerializationFile(project);
		if (file != null && file.exists()) {
			try {
				DataInputStream in = new DataInputStream(
						new BufferedInputStream(new FileInputStream(file)));
				try {
					String pluginID = in.readUTF();
					if (!pluginID.equals(DLTKCore.PLUGIN_ID))
						throw new IOException(Messages.build_wrongFileFormat);
					String kind = in.readUTF();
					if (!kind.equals("STATE")) //$NON-NLS-1$
						throw new IOException(Messages.build_wrongFileFormat);
					if (in.readBoolean())
						return ScriptBuilder.readState(project, in);
					if (ScriptBuilder.DEBUG)
						System.out
								.println("Saved state thinks last build failed for " + project.getName()); //$NON-NLS-1$
				} finally {
					in.close();
				}
			} catch (Exception e) {
				e.printStackTrace();
				throw new CoreException(
						new Status(
								IStatus.ERROR,
								DLTKCore.PLUGIN_ID,
								Platform.PLUGIN_ERROR,
								"Error reading last build state for project " + project.getName(), e)); //$NON-NLS-1$
			}
		} else if (ScriptBuilder.DEBUG) {
			if (file == null)
				System.out.println("Project does not exist: " + project); //$NON-NLS-1$
			else
				System.out
						.println("Build state file " + file.getPath() + " does not exist"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return null;
	}

	public static void recreatePersistedContainer(String propertyName,
			String containerString, boolean addToContainerValues) {
		int containerPrefixLength = BP_CONTAINER_PREFERENCES_PREFIX.length();
		int index = propertyName.indexOf('|', containerPrefixLength);
		if (containerString != null)
			containerString = containerString.trim();
		if (index > 0) {
			String projectName = propertyName.substring(containerPrefixLength,
					index).trim();
			IScriptProject project = getModelManager().getModel()
					.getScriptProject(projectName);
			IPath containerPath = new Path(propertyName.substring(index + 1)
					.trim());
			recreatePersistedContainer(project, containerPath, containerString,
					addToContainerValues);
		}
	}

	private static void recreatePersistedContainer(
			final IScriptProject project, final IPath containerPath,
			String containerString, boolean addToContainerValues) {
		if (!project.getProject().isAccessible())
			return; // avoid leaking deleted project's persisted container
		if (containerString == null) {
			getModelManager().containerPut(project, containerPath, null);
		} else {
			IBuildpathEntry[] entries;
			try {
				entries = ((ScriptProject) project).decodeBuildpath(
						containerString, null/*
											 * not interested in unknown
											 * elements
											 */);
			} catch (IOException e) {
				Util
						.log(
								e,
								"Could not recreate persisted container: \n" + containerString); //$NON-NLS-1$
				entries = ScriptProject.INVALID_BUILDPATH;
			}
			if (entries != ScriptProject.INVALID_BUILDPATH) {
				final IBuildpathEntry[] containerEntries = entries;
				IBuildpathContainer container = new IBuildpathContainer() {
					public IBuildpathEntry[] getBuildpathEntries(
							IScriptProject project) {
						return containerEntries;
					}

					public String getDescription(IScriptProject project) {
						return "Persisted container [" + containerPath + " for project [" + project.getElementName() + "]"; //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
					}

					public int getKind() {
						return 0;
					}

					public IPath getPath() {
						return containerPath;
					}

					public String toString() {
						return getDescription(project);
					}

					public IBuiltinModuleProvider getBuiltinProvider(
							IScriptProject project) {
						return null;
					}
				};
				if (addToContainerValues) {
					getModelManager().containerPut(project, containerPath,
							container);
				}
				Map projectContainers = (Map) getModelManager().previousSessionContainers
						.get(project);
				if (projectContainers == null) {
					projectContainers = new HashMap(1);
					getModelManager().previousSessionContainers.put(project,
							projectContainers);
				}
				projectContainers.put(containerPath, container);
			}
		}
	}

	/**
	 * Initialize preferences lookups for DLTKCore plugin.
	 */
	public void initializePreferences() {
		// Create lookups
		preferencesLookup[PREF_INSTANCE] = ((IScopeContext) new InstanceScope())
				.getNode(DLTKCore.PLUGIN_ID);
		preferencesLookup[PREF_DEFAULT] = ((IScopeContext) new DefaultScope())
				.getNode(DLTKCore.PLUGIN_ID);
		// Listen to instance preferences node removal from parent in order to
		// refresh stored one
		IEclipsePreferences.INodeChangeListener listener = new IEclipsePreferences.INodeChangeListener() {
			public void added(IEclipsePreferences.NodeChangeEvent event) {
				// do nothing
			}

			public void removed(IEclipsePreferences.NodeChangeEvent event) {
				if (event.getChild() == preferencesLookup[PREF_INSTANCE]) {
					preferencesLookup[PREF_INSTANCE] = ((IScopeContext) new InstanceScope())
							.getNode(DLTKCore.PLUGIN_ID);
					preferencesLookup[PREF_INSTANCE]
							.addPreferenceChangeListener(new EclipsePreferencesListener());
				}
			}
		};
		((IEclipsePreferences) preferencesLookup[PREF_INSTANCE].parent())
				.addNodeChangeListener(listener);
		preferencesLookup[PREF_INSTANCE]
				.addPreferenceChangeListener(new EclipsePreferencesListener());
		// Listen to default preferences node removal from parent in order to
		// refresh stored one
		listener = new IEclipsePreferences.INodeChangeListener() {
			public void added(IEclipsePreferences.NodeChangeEvent event) {
				// do nothing
			}

			public void removed(IEclipsePreferences.NodeChangeEvent event) {
				if (event.getChild() == preferencesLookup[PREF_DEFAULT]) {
					preferencesLookup[PREF_DEFAULT] = ((IScopeContext) new DefaultScope())
							.getNode(DLTKCore.PLUGIN_ID);
				}
			}
		};
		((IEclipsePreferences) preferencesLookup[PREF_DEFAULT].parent())
				.addNodeChangeListener(listener);
	}

	public void shutdown() {
		DLTKCore.getDefault().savePluginPreferences();
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		workspace.removeResourceChangeListener(this.deltaState);
		workspace.removeSaveParticipant(DLTKCore.getDefault());

		if (sourceModuleInfoCach != null) {
			sourceModuleInfoCach.stop();
		}
		if (this.indexManager != null) { // no more indexing
			this.indexManager.shutdown();
		}
		// wait for the initialization job to finish
		try {
			Job.getJobManager().join(DLTKCore.PLUGIN_ID, null);
		} catch (InterruptedException e) {
			// ignore
		}
	}

	public void removePerProjectInfo(ScriptProject scriptProject) {
		synchronized (this.perProjectInfos) { // use the perProjectInfo
			// collection as its own lock
			IProject project = scriptProject.getProject();
			PerProjectInfo info = (PerProjectInfo) this.perProjectInfos
					.get(project);
			if (info != null) {
				this.perProjectInfos.remove(project);
			}
		}
	}

	/**
	 * Sets the last built state for the given project, or null to reset it.
	 */
	public void setLastBuiltState(IProject project, Object state) {
		if (DLTKLanguageManager.hasScriptNature(project)) {
			// should never be requested on non-script projects
			PerProjectInfo info = getPerProjectInfo(project, true /*
																 * create if
																 * missing
																 */);
			info.triedRead = true; // no point trying to re-read once using
			// setter
			info.savedState = state;
		}
		if (state == null) { // delete state file to ensure a full build
			// happens if the workspace crashes
			try {
				File file = getSerializationFile(project);
				if (file != null && file.exists())
					file.delete();
			} catch (SecurityException se) {
				// could not delete file: cannot do much more
			}
		}
	}

	/**
	 * Returns the File to use for saving and restoring the last built state for
	 * the given project.
	 */
	private File getSerializationFile(IProject project) {
		if (!project.exists())
			return null;
		IPath workingLocation = project.getWorkingLocation(DLTKCore.PLUGIN_ID);
		return workingLocation.append("state.dat").toFile(); //$NON-NLS-1$
	}

	// If modified, also modify the method getDefaultOptionsNoInitialization()
	public Hashtable getDefaultOptions() {
		Hashtable defaultOptions = new Hashtable(10);
		// see
		// DLTKCorePreferenceInitializer#initializeDefaultPluginPreferences()
		// for changing default settings
		// If modified, also modify the method
		// getDefaultOptionsNoInitialization()
		IEclipsePreferences defaultPreferences = getDefaultPreferences();
		// initialize preferences to their default
		Iterator iterator = this.optionNames.iterator();
		while (iterator.hasNext()) {
			String propertyName = (String) iterator.next();
			String value = defaultPreferences.get(propertyName, null);
			if (value != null)
				defaultOptions.put(propertyName, value);
		}
		// get encoding through resource plugin
		defaultOptions.put(DLTKCore.CORE_ENCODING, DLTKCore.getEncoding());
		return defaultOptions;
	}

	/**
	 * Get default eclipse preference for DLTKCore plugin.
	 */
	public IEclipsePreferences getDefaultPreferences() {
		return preferencesLookup[PREF_DEFAULT];
	}

	// Do not modify without modifying getDefaultOptions()
	private Hashtable getDefaultOptionsNoInitialization() {
		System.err
				.println("Add language dependent compiler options. Or implement it in another whan in DLTK way..."); //$NON-NLS-1$
		Map defaultOptionsMap = new HashMap(); // compiler defaults
		return new Hashtable(defaultOptionsMap);
	}

	public IBuildpathContainer getBuildpathContainer(IPath containerPath,
			IScriptProject project) throws ModelException {
		IBuildpathContainer container = containerGet(project, containerPath);
		if (container == null) {
			if (this.batchContainerInitializations) {
				// avoid deep recursion while initializaing container on
				// workspace restart
				// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=60437)
				this.batchContainerInitializations = false;
				return initializeAllContainers(project, containerPath);
			}
			return initializeContainer(project, containerPath);
		}
		return container;
	}

	public synchronized IBuildpathContainer containerGet(
			IScriptProject project, IPath containerPath) {
		// check initialization in progress first
		HashSet projectInitializations = containerInitializationInProgress(project);
		if (projectInitializations.contains(containerPath)) {
			return CONTAINER_INITIALIZATION_IN_PROGRESS;
		}
		Map projectContainers = (Map) this.containers.get(project);
		if (projectContainers == null) {
			return null;
		}
		IBuildpathContainer container = (IBuildpathContainer) projectContainers
				.get(containerPath);
		return container;
	}

	/*
	 * Returns the set of container paths for the given project that are being
	 * initialized in the current thread.
	 */
	private HashSet containerInitializationInProgress(IScriptProject project) {
		Map initializations = (Map) this.containerInitializationInProgress
				.get();
		if (initializations == null) {
			initializations = new HashMap();
			this.containerInitializationInProgress.set(initializations);
		}
		HashSet projectInitializations = (HashSet) initializations.get(project);
		if (projectInitializations == null) {
			projectInitializations = new HashSet();
			initializations.put(project, projectInitializations);
		}
		return projectInitializations;
	}

	/*
	 * Initialize all container at the same time as the given container. Return
	 * the container for the given path and project.
	 */
	private IBuildpathContainer initializeAllContainers(
			IScriptProject scriptProjectToInit, IPath containerToInit)
			throws ModelException {
		/*
		 * if (BP_RESOLVE_VERBOSE) { Util.verbose( "CPContainer INIT - batching
		 * containers initialization\n" + //$NON-NLS-1$ " project to init: " +
		 * scriptProjectToInit.getElementName() + '\n' + //$NON-NLS-1$ "
		 * container path to init: " + containerToInit); //$NON-NLS-1$ }
		 */
		// collect all container paths
		final HashMap allContainerPaths = new HashMap();
		IProject[] projects = ResourcesPlugin.getWorkspace().getRoot()
				.getProjects();
		for (int i = 0, length = projects.length; i < length; i++) {
			IProject project = projects[i];
			if (!DLTKLanguageManager.hasScriptNature(project))
				continue;
			ScriptProject scriptProject = new ScriptProject(project, getModel());
			HashSet paths = null;
			IBuildpathEntry[] rawBuildpath = scriptProject.getRawBuildpath();
			for (int j = 0, length2 = rawBuildpath.length; j < length2; j++) {
				IBuildpathEntry entry = rawBuildpath[j];
				IPath path = entry.getPath();
				if (entry.getEntryKind() == IBuildpathEntry.BPE_CONTAINER
						&& containerGet(scriptProject, path) == null) {
					if (paths == null) {
						paths = new HashSet();
						allContainerPaths.put(scriptProject, paths);
					}
					paths.add(path);
				}
			}
		}
		HashSet containerPaths = (HashSet) allContainerPaths
				.get(scriptProjectToInit);
		if (containerPaths == null) {
			containerPaths = new HashSet();
			allContainerPaths.put(scriptProjectToInit, containerPaths);
		}
		containerPaths.add(containerToInit);
		// end block
		// mark all containers as being initialized
		this.containerInitializationInProgress.set(allContainerPaths);
		// initialize all containers
		boolean ok = false;
		try {
			// if possible run inside an IWokspaceRunnable with AVOID_UPATE to
			// avoid unwanted builds
			// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=118507)
			IWorkspaceRunnable runnable = new IWorkspaceRunnable() {
				public void run(IProgressMonitor monitor) throws CoreException {
					Set keys = allContainerPaths.keySet();
					int length = keys.size();
					IScriptProject[] scriptProjects = new IScriptProject[length]; // clone
					// as
					// the
					// following
					// will
					// have
					// a
					// side
					// effect
					keys.toArray(scriptProjects);
					for (int i = 0; i < length; i++) {
						IScriptProject scriptProject = scriptProjects[i];
						HashSet pathSet = (HashSet) allContainerPaths
								.get(scriptProject);
						if (pathSet == null)
							continue;
						int length2 = pathSet.size();
						IPath[] paths = new IPath[length2];
						pathSet.toArray(paths); // clone as the following will
						// have a side effect
						for (int j = 0; j < length2; j++) {
							IPath path = paths[j];
							initializeContainer(scriptProject, path);
						}
					}
				}
			};
			IWorkspace workspace = ResourcesPlugin.getWorkspace();
			if (workspace.isTreeLocked())
				runnable.run(null/* no progress available */);
			else
				workspace.run(runnable, null/* don't take any lock */,
						IWorkspace.AVOID_UPDATE, null/*
													 * no progress available
													 * here
													 */);
			ok = true;
		} catch (CoreException e) {
			// ignore
			System.err.println("Exception while initializing all containers"); //$NON-NLS-1$
			// Util.log(e, "Exception while initializing all containers");
			// //$NON-NLS-1$
		} finally {
			if (!ok) {
				// if we're being traversed by an exception, ensure that that
				// containers are
				// no longer marked as initialization in progress
				// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=66437)
				this.containerInitializationInProgress.set(null);
			}
		}
		return containerGet(scriptProjectToInit, containerToInit);
	}

	IBuildpathContainer initializeContainer(IScriptProject project,
			IPath containerPath) throws ModelException {
		IBuildpathContainer container = null;
		final BuildpathContainerInitializer initializer = DLTKCore
				.getBuildpathContainerInitializer(containerPath.segment(0));
		if (initializer != null) {
			if (BP_RESOLVE_VERBOSE) {
				Util.verbose("BPContainer INIT - triggering initialization\n" + //$NON-NLS-1$
						"	project: " + project.getElementName() + '\n' + //$NON-NLS-1$
						"	container path: " + containerPath + '\n' + //$NON-NLS-1$
						"	initializer: " + initializer + '\n' + //$NON-NLS-1$
						"	invocation stack trace:"); //$NON-NLS-1$
				new Exception("<Fake exception>").printStackTrace(System.out); //$NON-NLS-1$
			}
			// PerformanceStats stats = null;
			containerPut(project, containerPath,
					CONTAINER_INITIALIZATION_IN_PROGRESS); // avoid
			// initialization
			// cycles
			boolean ok = false;
			try {
				// let OperationCanceledException go through
				// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59363)
				initializer.initialize(containerPath, project);
				// retrieve value (if initialization was successful)
				container = containerGet(project, containerPath);
				if (container == CONTAINER_INITIALIZATION_IN_PROGRESS)
					return null; // break cycle
				ok = true;
			} catch (CoreException e) {
				if (e instanceof ModelException) {
					throw (ModelException) e;
				} else {
					throw new ModelException(e);
				}
			} catch (RuntimeException e) {
				if (ModelManager.BP_RESOLVE_VERBOSE) {
					e.printStackTrace();
				}
				throw e;
			} catch (Error e) {
				if (ModelManager.BP_RESOLVE_VERBOSE) {
					e.printStackTrace();
				}
				throw e;
			} finally {
				if (!ok) {
					// just remove initialization in progress and keep previous
					// session container so as to avoid a full build
					// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=92588
					containerRemoveInitializationInProgress(project,
							containerPath);
					if (BP_RESOLVE_VERBOSE) {
						if (container == CONTAINER_INITIALIZATION_IN_PROGRESS) {
							Util
									.verbose("CPContainer INIT - FAILED (initializer did not initialize container)\n" + //$NON-NLS-1$
											"	project: " //$NON-NLS-1$
											+ project.getElementName() + '\n' + //$NON-NLS-1$
											"	container path: " //$NON-NLS-1$
											+ containerPath + '\n' + //$NON-NLS-1$
											"	initializer: " + initializer); //$NON-NLS-1$
						} else {
							Util
									.verbose("CPContainer INIT - FAILED (see exception above)\n" + //$NON-NLS-1$
											"	project: " //$NON-NLS-1$
											+ project.getElementName() + '\n' + //$NON-NLS-1$
											"	container path: " //$NON-NLS-1$
											+ containerPath + '\n' + //$NON-NLS-1$
											"	initializer: " + initializer); //$NON-NLS-1$
						}
					}
				}
			}
			if (BP_RESOLVE_VERBOSE) {
				StringBuffer buffer = new StringBuffer();
				buffer.append("CPContainer INIT - after resolution\n"); //$NON-NLS-1$
				buffer.append("	project: " + project.getElementName() + '\n'); //$NON-NLS-1$
				buffer.append("	container path: " + containerPath + '\n'); //$NON-NLS-1$
				if (container != null) {
					buffer
							.append("	container: " + container.getDescription(project) + " {\n"); //$NON-NLS-2$//$NON-NLS-1$
					IBuildpathEntry[] entries = container
							.getBuildpathEntries(project);
					if (entries != null) {
						for (int i = 0; i < entries.length; i++) {
							buffer.append("		" + entries[i] + '\n'); //$NON-NLS-1$
						}
					}
					buffer.append("	}");//$NON-NLS-1$
				} else {
					buffer.append("	container: {unbound}");//$NON-NLS-1$
				}
				Util.verbose(buffer.toString());
			}
		} else {
			if (BP_RESOLVE_VERBOSE) {
				Util.verbose("CPContainer INIT - no initializer found\n" + //$NON-NLS-1$
						"	project: " + project.getElementName() + '\n' + //$NON-NLS-1$
						"	container path: " + containerPath); //$NON-NLS-1$
			}
		}
		return container;
	}

	/**
	 * Returns a persisted container from previous session if any. Note that it
	 * is not the original container from previous session (i.e. it did not get
	 * serialized) but rather a summary of its entries recreated for CP
	 * initialization purpose. As such it should not be stored into container
	 * caches.
	 */
	public IBuildpathContainer getPreviousSessionContainer(IPath containerPath,
			IScriptProject project) {
		Map previousContainerValues = (Map) this.previousSessionContainers
				.get(project);
		if (previousContainerValues != null) {
			IBuildpathContainer previousContainer = (IBuildpathContainer) previousContainerValues
					.get(containerPath);
			if (previousContainer != null) {
				if (ModelManager.BP_RESOLVE_VERBOSE) {
					StringBuffer buffer = new StringBuffer();
					buffer
							.append("CPContainer INIT - reentering access to project container during its initialization, will see previous value\n"); //$NON-NLS-1$ 
					buffer
							.append("	project: " + project.getElementName() + '\n'); //$NON-NLS-1$
					buffer.append("	container path: " + containerPath + '\n'); //$NON-NLS-1$
					buffer.append("	previous value: "); //$NON-NLS-1$
					buffer.append(previousContainer.getDescription(project));
					buffer.append(" {\n"); //$NON-NLS-1$
					IBuildpathEntry[] entries = previousContainer
							.getBuildpathEntries(project);
					if (entries != null) {
						for (int j = 0; j < entries.length; j++) {
							buffer.append(" 		"); //$NON-NLS-1$
							buffer.append(entries[j]);
							buffer.append('\n');
						}
					}
					buffer.append(" 	}"); //$NON-NLS-1$
					Util.verbose(buffer.toString());
					new Exception("<Fake exception>").printStackTrace(System.out); //$NON-NLS-1$
				}
				return previousContainer;
			}
		}
		return null; // break cycle if none found
	}

	public synchronized void containerPut(IScriptProject project,
			IPath containerPath, IBuildpathContainer container) {
		// set/unset the initialization in progress
		if (container == CONTAINER_INITIALIZATION_IN_PROGRESS) {
			HashSet projectInitializations = containerInitializationInProgress(project);
			projectInitializations.add(containerPath);
			// do not write out intermediate initialization value
			return;
		} else {
			containerRemoveInitializationInProgress(project, containerPath);
			Map projectContainers = (Map) this.containers.get(project);
			if (projectContainers == null) {
				projectContainers = new HashMap(1);
				this.containers.put(project, projectContainers);
			}
			if (container == null) {
				projectContainers.remove(containerPath);
			} else {
				projectContainers.put(containerPath, container);
			}
			// discard obsoleted information about previous session
			Map previousContainers = (Map) this.previousSessionContainers
					.get(project);
			if (previousContainers != null) {
				previousContainers.remove(containerPath);
			}
		}
		// container values are persisted in preferences during save operations,
		// see #saving(ISaveContext)
	}

	/*
	 * The given project is being removed. Remove all containers for this
	 * project from the cache.
	 */
	public synchronized void containerRemove(IScriptProject project) {
		Map initializations = (Map) this.containerInitializationInProgress
				.get();
		if (initializations != null) {
			initializations.remove(project);
		}
		this.containers.remove(project);
	}

	private void containerRemoveInitializationInProgress(
			IScriptProject project, IPath containerPath) {
		HashSet projectInitializations = containerInitializationInProgress(project);
		projectInitializations.remove(containerPath);
		if (projectInitializations.size() == 0) {
			Map initializations = (Map) this.containerInitializationInProgress
					.get();
			initializations.remove(project);
		}
	}

	/*
	 * Optimize startup case where a container for 1 project is initialized at a
	 * time with the same entries as on shutdown.
	 */
	public boolean containerPutIfInitializingWithSameEntries(
			IPath containerPath, IScriptProject[] projects,
			IBuildpathContainer[] respectiveContainers) {
		int projectLength = projects.length;
		if (projectLength != 1)
			return false;
		final IBuildpathContainer container = respectiveContainers[0];
		if (container == null)
			return false;
		final IScriptProject project = projects[0];
		if (!containerInitializationInProgress(project).contains(containerPath))
			return false;
		IBuildpathContainer previousSessionContainer = getPreviousSessionContainer(
				containerPath, project);
		final IBuildpathEntry[] newEntries = container
				.getBuildpathEntries(project);
		if (previousSessionContainer == null)
			if (newEntries.length == 0) {
				containerPut(project, containerPath, container);
				return true;
			} else {
				return false;
			}
		final IBuildpathEntry[] oldEntries = previousSessionContainer
				.getBuildpathEntries(project);
		if (oldEntries.length != newEntries.length)
			return false;
		for (int i = 0, length = newEntries.length; i < length; i++) {
			if (!newEntries[i].equals(oldEntries[i])) {
				if (BP_RESOLVE_VERBOSE) {
					Util.verbose("CPContainer SET  - missbehaving container\n" + //$NON-NLS-1$
							"	container path: " //$NON-NLS-1$
							+ containerPath
							+ '\n'
							+ //$NON-NLS-1$
							"	projects: {" //$NON-NLS-1$
							+ //$NON-NLS-1$
							Util.toString(projects, new Util.Displayable() {
								public String displayString(Object o) {
									return ((IScriptProject) o)
											.getElementName();
								}
							})
							+ "}\n	values on previous session: {\n" + //$NON-NLS-1$
							Util.toString(respectiveContainers,
									new Util.Displayable() {
										public String displayString(Object o) {
											StringBuffer buffer = new StringBuffer(
													"		"); //$NON-NLS-1$
											if (o == null) {
												buffer.append("<null>"); //$NON-NLS-1$
												return buffer.toString();
											}
											buffer.append(container
													.getDescription(project));
											buffer.append(" {\n"); //$NON-NLS-1$
											for (int j = 0; j < oldEntries.length; j++) {
												buffer.append(" 			"); //$NON-NLS-1$
												buffer.append(oldEntries[j]);
												buffer.append('\n');
											}
											buffer.append(" 		}"); //$NON-NLS-1$
											return buffer.toString();
										}
									})
							+ "}\n	new values: {\n" + //$NON-NLS-1$
							Util.toString(respectiveContainers,
									new Util.Displayable() {
										public String displayString(Object o) {
											StringBuffer buffer = new StringBuffer(
													"		"); //$NON-NLS-1$
											if (o == null) {
												buffer.append("<null>"); //$NON-NLS-1$
												return buffer.toString();
											}
											buffer.append(container
													.getDescription(project));
											buffer.append(" {\n"); //$NON-NLS-1$
											for (int j = 0; j < newEntries.length; j++) {
												buffer.append(" 			"); //$NON-NLS-1$
												buffer.append(newEntries[j]);
												buffer.append('\n');
											}
											buffer.append(" 		}"); //$NON-NLS-1$
											return buffer.toString();
										}
									}) + "\n	}"); //$NON-NLS-1$
				}
				return false;
			}
		}
		containerPut(project, containerPath, container);
		return true;
	}

	/**
	 * Returns the open ZipFile at the given path. If the ZipFile does not yet
	 * exist, it is created, opened, and added to the cache of open ZipFiles.
	 * 
	 * NOTE: closeZipFile() must be called for the resulting ZipFile, when the
	 * client is done using it.
	 * 
	 * The path must be a file system path if representing an external zip, or
	 * it must be an absolute workspace relative path if representing a zip
	 * inside the workspace.
	 * 
	 * @exception CoreException
	 *                If unable to create/open the ZipFile
	 */
	public ZipFile getZipFile(IPath path) throws CoreException {
		HashMap map;
		ZipFile zipFile;
		if ((map = (HashMap) this.zipFiles.get()) != null
				&& (zipFile = (ZipFile) map.get(path)) != null) {
			return zipFile;
		}
		File localFile = null;
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		IResource file = root.findMember(path);
		if (file != null) {
			// internal resource
			URI location;
			if (file.getType() != IResource.FILE
					|| (location = file.getLocationURI()) == null) {
				throw new CoreException(new Status(IStatus.ERROR,
						DLTKCore.PLUGIN_ID, -1, Messages.bind(
								Messages.file_notFound, path.toString()), null));
			}
			localFile = Util.toLocalFile(location, null/*
														 * no progress
														 * availaible
														 */);
			if (localFile == null)
				throw new CoreException(new Status(IStatus.ERROR,
						DLTKCore.PLUGIN_ID, -1, Messages.bind(
								Messages.file_notFound, path.toString()), null));
		} else {
			// external resource -> it is ok to use toFile()
			localFile = path.toFile();
		}
		try {
			if (ZIP_ACCESS_VERBOSE) {
				System.out
						.println("(" + Thread.currentThread() + ") [ModelManager.getZipFile(IPath)] Creating ZipFile on " + localFile); //$NON-NLS-1$ //$NON-NLS-2$
			}
			zipFile = new ZipFile(localFile);
			if (map != null) {
				map.put(path, zipFile);
			}
			return zipFile;
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR,
					DLTKCore.PLUGIN_ID, -1, Messages.status_IOException, e));
		}
	}

	/**
	 * Starts caching ZipFiles. Ignores if there are already clients.
	 */
	public void cacheZipFiles() {
		if (this.zipFiles.get() != null)
			return;
		this.zipFiles.set(new HashMap());
	}

	public void closeZipFile(ZipFile zipFile) {
		if (zipFile == null)
			return;
		if (this.zipFiles.get() != null) {
			return; // zip file will be closed by call to flushZipFiles
		}
		try {
			if (ModelManager.ZIP_ACCESS_VERBOSE) {
				System.out
						.println("(" + Thread.currentThread() + ") [ModelManager.closeZipFile(ZipFile)] Closing ZipFile on " + zipFile.getName()); //$NON-NLS-1$	//$NON-NLS-2$
			}
			zipFile.close();
		} catch (IOException e) {
			// problem occured closing zip file: cannot do much more
		}
	}

	public void doneSaving(ISaveContext context) {
		// nothing
	}

	public void prepareToSave(ISaveContext context) throws CoreException {
		// TODO Auto-generated method stub
	}

	public void rollback(ISaveContext context) {
		// TODO Auto-generated method stub
	}

	private void traceContainers(String action, long start) {
		Long delta = new Long(System.currentTimeMillis() - start);
		Long length = new Long(getContainersFile().length());
		String pattern = "{0} {1} bytes in containers.dat in {2}ms"; //$NON-NLS-1$
		String message = MessageFormat.format(pattern, new Object[] { action,
				length, delta });
		System.out.println(message);
	}

	public void saving(ISaveContext context) throws CoreException {
		// save variable and container values on snapshot/full save
		long start = -1;
		if (VERBOSE)
			start = System.currentTimeMillis();
		savesContainers();
		if (VERBOSE)
			traceContainers("Saved", start); //$NON-NLS-1$
		if (context.getKind() == ISaveContext.FULL_SAVE) {
			// will need delta since this save (see
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=38658)
			context.needDelta();
			// clean up indexes on workspace full save
			// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=52347)
			IndexManager manager = this.indexManager;
			if (manager != null
			// don't force initialization of workspace scope as we could be
					// shutting down
					// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=93941)
					&& this.workspaceScope != null) {
				manager.cleanUpIndexes();
			}
		}
		IProject savedProject = context.getProject();
		if (savedProject != null) {
			if (!ScriptProject.hasScriptNature(savedProject))
				return; // ignore
			PerProjectInfo info = getPerProjectInfo(savedProject, true /*
																		 * create
																		 * info
																		 */);
			saveState(info, context);
			info.rememberExternalLibTimestamps();
			return;
		}
		ArrayList vStats = null; // lazy initialized
		ArrayList values = null;
		synchronized (this.perProjectInfos) {
			values = new ArrayList(this.perProjectInfos.values());
		}
		if (values != null) {
			Iterator iterator = values.iterator();
			while (iterator.hasNext()) {
				try {
					PerProjectInfo info = (PerProjectInfo) iterator.next();
					saveState(info, context);
					info.rememberExternalLibTimestamps();
				} catch (CoreException e) {
					if (vStats == null)
						vStats = new ArrayList();
					vStats.add(e.getStatus());
				}
			}
		}
		if (vStats != null) {
			IStatus[] stats = new IStatus[vStats.size()];
			vStats.toArray(stats);
			throw new CoreException(
					new MultiStatus(DLTKCore.PLUGIN_ID, IStatus.ERROR, stats,
							Messages.build_cannotSaveStates, null));
		}
		// save external libs timestamps
		this.deltaState.saveExternalLibTimeStamps();
	}

	private File getContainersFile() {
		return DLTKCore.getPlugin().getStateLocation()
				.append("Containers.dat").toFile(); //$NON-NLS-1$
	}

	private void saveState(PerProjectInfo info, ISaveContext context)
			throws CoreException {
		// passed this point, save actions are non trivial
		if (context.getKind() == ISaveContext.SNAPSHOT)
			return;
		// save built state
		if (info.triedRead)
			saveBuiltState(info);
	}

	/**
	 * Saves the built state for the project.
	 */
	private void saveBuiltState(PerProjectInfo info) throws CoreException {
		if (ScriptBuilder.DEBUG)
			System.out.println(Messages.bind(Messages.build_saveStateProgress,
					info.project.getName()));
		File file = getSerializationFile(info.project);
		if (file == null)
			return;
		long t = System.currentTimeMillis();
		try {
			DataOutputStream out = new DataOutputStream(
					new BufferedOutputStream(new FileOutputStream(file)));
			try {
				out.writeUTF(DLTKCore.PLUGIN_ID);
				out.writeUTF("STATE"); //$NON-NLS-1$
				if (info.savedState == null) {
					out.writeBoolean(false);
				} else {
					out.writeBoolean(true);
					ScriptBuilder.writeState(info.savedState, out);
				}
			} finally {
				out.close();
			}
		} catch (RuntimeException e) {
			try {
				file.delete();
			} catch (SecurityException se) {
				// could not delete file: cannot do much more
			}
			throw new CoreException(new Status(IStatus.ERROR,
					DLTKCore.PLUGIN_ID, Platform.PLUGIN_ERROR, Messages.bind(
							Messages.build_cannotSaveState, info.project
									.getName()), e));
		} catch (IOException e) {
			try {
				file.delete();
			} catch (SecurityException se) {
				// could not delete file: cannot do much more
			}
			throw new CoreException(new Status(IStatus.ERROR,
					DLTKCore.PLUGIN_ID, Platform.PLUGIN_ERROR, Messages.bind(
							Messages.build_cannotSaveState, info.project
									.getName()), e));
		}
		if (ScriptBuilder.DEBUG) {
			t = System.currentTimeMillis() - t;
			System.out.println(Messages.bind(Messages.build_saveStateComplete,
					String.valueOf(t)));
		}
	}

	private void savesContainers() throws CoreException {
		File file = getContainersFile();
		DataOutputStream out = null;
		try {
			out = new DataOutputStream(new BufferedOutputStream(
					new FileOutputStream(file)));
			out.writeInt(CONTAINERS_FILE_VERSION);
			new ContainersSaveHelper(out).save();
			// old code retained for performance comparisons
			// containers
			IScriptProject[] projects = getModel().getScriptProjects();
			int length = projects.length;
			out.writeInt(length);
			for (int i = 0; i < length; i++) {
				IScriptProject project = projects[i];
				// clone while iterating (see
				// https://bugs.eclipse.org/bugs/show_bug.cgi?id=59638)
				Map projectContainers = containerClone(project);
				out.writeUTF(project.getElementName());
				if (projectContainers == null) {
					out.writeInt(0);
					continue;
				}
				HashMap containersToSave = new HashMap();
				for (Iterator iterator = projectContainers.keySet().iterator(); iterator
						.hasNext();) {
					IPath containerPath = (IPath) iterator.next();
					IBuildpathContainer container = (IBuildpathContainer) projectContainers
							.get(containerPath);
					String containerString = null;
					try {
						if (container == null) {
							// container has not been initialized yet, use
							// previous session value
							// (see
							//https://bugs.eclipse.org/bugs/show_bug.cgi?id=73969
							// )
							container = getPreviousSessionContainer(
									containerPath, project);
						}
						if (container != null) {
							IBuildpathEntry[] entries = container
									.getBuildpathEntries(project);
							containerString = ((ScriptProject) project)
									.encodeBuildpath(entries, false, null/*
																		 * not
																		 * interested
																		 * in
																		 * unknown
																		 * elements
																		 */);
						}
					} catch (ModelException e) {
						// could not encode entry: will not persist
						Util
								.log(
										e,
										"Could not persist container " + containerPath + " for project " + project.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$
					}
					if (containerString != null)
						containersToSave.put(containerPath, containerString);
				}
				out.writeInt(containersToSave.size());
				Iterator iterator = containersToSave.keySet().iterator();
				while (iterator.hasNext()) {
					IPath containerPath = (IPath) iterator.next();
					out.writeUTF(containerPath.toPortableString());
					String containerString = (String) containersToSave
							.get(containerPath);
					out.writeInt(containerString.length());
					out.writeBytes(containerString);
				}
			}
		} catch (IOException e) {
			IStatus status = new Status(IStatus.ERROR, DLTKCore.PLUGIN_ID,
					IStatus.ERROR,
					"Problems while saving variables and containers", e); //$NON-NLS-1$
			throw new CoreException(status);
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (IOException e) {
					// nothing we can do: ignore
				}
			}
		}
	}

	protected synchronized void resetZIPTypeCache() {
		this.cache.resetZIPTypeCache();
	}

	public DLTKWorkspaceScope getWorkspaceScope(IDLTKLanguageToolkit toolkit) {
		if (this.workspaceScope == null) {
			this.workspaceScope = new HashMap();
		}
		if (this.workspaceScope.containsKey(toolkit)) {
			return (DLTKWorkspaceScope) this.workspaceScope.get(toolkit);
		} else {
			DLTKWorkspaceScope scope = new DLTKWorkspaceScope(toolkit);
			this.workspaceScope.put(toolkit, scope);
			return scope;
		}
	}

	private final class ContainersSaveHelper {
		private final HashtableOfObjectToInt buildpathEntryIds;
		// -> int
		private final DataOutputStream out;
		private final HashtableOfObjectToInt stringIds; // Strings -> int

		ContainersSaveHelper(DataOutputStream out) {
			super();
			this.buildpathEntryIds = new HashtableOfObjectToInt();
			this.out = out;
			this.stringIds = new HashtableOfObjectToInt();
		}

		void save() throws IOException, ModelException {
			saveProjects(ModelManager.this.getModel().getScriptProjects());
		}

		private void saveAccessRule(BuildpathAccessRule rule)
				throws IOException {
			saveInt(rule.problemId);
			savePath(rule.getPattern());
		}

		private void saveAccessRules(IAccessRule[] rules) throws IOException {
			int count = rules == null ? 0 : rules.length;
			saveInt(count);
			for (int i = 0; i < count; ++i)
				saveAccessRule((BuildpathAccessRule) rules[i]);
		}

		private void saveAttribute(IBuildpathAttribute attribute)
				throws IOException {
			saveString(attribute.getName());
			saveString(attribute.getValue());
		}

		private void saveAttributes(IBuildpathAttribute[] attributes)
				throws IOException {
			int count = attributes == null ? 0 : attributes.length;
			saveInt(count);
			for (int i = 0; i < count; ++i)
				saveAttribute(attributes[i]);
		}

		private void saveBuildpathEntries(IBuildpathEntry[] entries)
				throws IOException {
			int count = entries == null ? 0 : entries.length;
			saveInt(count);
			for (int i = 0; i < count; ++i)
				saveBuildpathEntry(entries[i]);
		}

		private void saveBuildpathEntry(IBuildpathEntry entry)
				throws IOException {
			if (saveNewId(entry, this.buildpathEntryIds)) {
				saveInt(entry.getContentKind());
				saveInt(entry.getEntryKind());
				savePath(entry.getPath());
				savePaths(entry.getInclusionPatterns());
				savePaths(entry.getExclusionPatterns());
				this.out.writeBoolean(entry.isExported());
				this.out.writeBoolean(entry.isExternal());
				saveAccessRules(entry.getAccessRules());
				this.out.writeBoolean(entry.combineAccessRules());
				saveAttributes(entry.getExtraAttributes());
			}
		}

		private void saveContainers(IScriptProject project, Map containerMap)
				throws IOException {
			saveInt(containerMap.size());
			for (Iterator i = containerMap.entrySet().iterator(); i.hasNext();) {
				Entry entry = (Entry) i.next();
				IPath path = (IPath) entry.getKey();
				IBuildpathContainer container = (IBuildpathContainer) entry
						.getValue();
				IBuildpathEntry[] cpEntries = null;
				if (container == null) {
					// container has not been initialized yet, use previous
					// session value
					// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=73969)
					container = ModelManager.this.getPreviousSessionContainer(
							path, project);
				}
				if (container != null)
					cpEntries = container.getBuildpathEntries(project);
				savePath(path);
				saveBuildpathEntries(cpEntries);
			}
		}

		private void saveInt(int value) throws IOException {
			this.out.writeInt(value);
		}

		private boolean saveNewId(Object key, HashtableOfObjectToInt map)
				throws IOException {
			int id = map.get(key);
			if (id == -1) {
				int newId = map.size();
				map.put(key, newId);
				saveInt(newId);
				return true;
			} else {
				saveInt(id);
				return false;
			}
		}

		private void savePath(IPath path) throws IOException {
			if (path == null) {
				this.out.writeBoolean(true);
			} else {
				this.out.writeBoolean(false);
				saveString(path.toPortableString());
			}
		}

		private void savePaths(IPath[] paths) throws IOException {
			int count = paths == null ? 0 : paths.length;
			saveInt(count);
			for (int i = 0; i < count; ++i)
				savePath(paths[i]);
		}

		private void saveProjects(IScriptProject[] projects)
				throws IOException, ModelException {
			int count = projects.length;
			saveInt(count);
			for (int i = 0; i < count; ++i) {
				IScriptProject project = projects[i];
				saveString(project.getElementName());
				Map containerMap = (Map) ModelManager.this.containers
						.get(project);
				if (containerMap == null) {
					containerMap = Collections.EMPTY_MAP;
				} else {
					// clone while iterating
					// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=59638)
					containerMap = new HashMap(containerMap);
				}
				saveContainers(project, containerMap);
			}
		}

		private void saveString(String string) throws IOException {
			if (saveNewId(string, this.stringIds))
				this.out.writeUTF(string);
		}
	}

	private synchronized Map containerClone(IScriptProject project) {
		Map originalProjectContainers = (Map) this.containers.get(project);
		if (originalProjectContainers == null)
			return null;
		Map projectContainers = new HashMap(originalProjectContainers.size());
		projectContainers.putAll(originalProjectContainers);
		return projectContainers;
	}

	public void loadContainers() throws CoreException {
		// backward compatibility, load variables and containers from
		// preferences into cache
		loadContainers(getDefaultPreferences());
		loadContainers(getInstancePreferences());
		// load variables and containers from saved file into cache
		File file = getContainersFile();
		DataInputStream in = null;
		try {
			in = new DataInputStream(new BufferedInputStream(
					new FileInputStream(file)));
			switch (in.readInt()) {
			case 1:
				new ContainersLoadHelper(in).load();
				break;
			}
		} catch (IOException e) {
			if (file.exists())
				Util.log(e, "Unable to read variable and containers file"); //$NON-NLS-1$
		} catch (RuntimeException e) {
			if (file.exists())
				Util
						.log(e,
								"Unable to read variable and containers file (file is corrupt)"); //$NON-NLS-1$
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// nothing we can do: ignore
				}
			}
		}
		// override persisted values for containers which have a registered
		// initializer
		containersReset(getRegisteredContainerIDs());
	}

	private void loadContainers(IEclipsePreferences preferences) {
		try {
			// only get variable from preferences not set to their default
			String[] propertyNames = preferences.keys();

			for (int i = 0; i < propertyNames.length; i++) {
				String propertyName = propertyNames[i];
				if (propertyName.startsWith(BP_CONTAINER_PREFERENCES_PREFIX)) {
					String propertyValue = preferences.get(propertyName, null);
					if (propertyValue != null) {
						// cleanup old preferences
						preferences.remove(propertyName);

						// recreate container
						recreatePersistedContainer(propertyName, propertyValue,
								true/* add to container values */);
					}
				}
			}
		} catch (BackingStoreException e1) {
			// TODO (frederic) see if it's necessary to report this failure...
		}
	}

	private static final class PersistedBuildpathContainer implements
			IBuildpathContainer {
		private final IPath containerPath;
		private final IBuildpathEntry[] entries;
		private final IScriptProject project;

		PersistedBuildpathContainer(IScriptProject project,
				IPath containerPath, IBuildpathEntry[] entries) {
			super();
			this.containerPath = containerPath;
			this.entries = entries;
			this.project = project;
		}

		public IBuildpathEntry[] getBuildpathEntries(IScriptProject project) {
			return entries;
		}

		public String getDescription(IScriptProject prj) {
			return "Persisted container [" + containerPath //$NON-NLS-1$
					+ " for project [" + project.getElementName() //$NON-NLS-1$
					+ "]]"; //$NON-NLS-1$  
		}

		public int getKind() {
			return 0;
		}

		public IPath getPath() {
			return containerPath;
		}

		public String toString() {
			return getDescription(project);
		}

		public IBuiltinModuleProvider getBuiltinProvider(IScriptProject project) {
			return null;
		}
	}

	private final class ContainersLoadHelper {
		private static final int ARRAY_INCREMENT = 200;
		private IBuildpathEntry[] allBuildpathEntries;
		private int allBuildpathEntryCount;
		private final Map allPaths; // String -> IPath
		private String[] allStrings;
		private int allStringsCount;
		private final DataInputStream in;

		ContainersLoadHelper(DataInputStream in) {
			super();
			this.allBuildpathEntries = null;
			this.allBuildpathEntryCount = 0;
			this.allPaths = new HashMap();
			this.allStrings = null;
			this.allStringsCount = 0;
			this.in = in;
		}

		void load() throws IOException {
			loadProjects(ModelManager.this.getModel());
		}

		private IAccessRule loadAccessRule() throws IOException {
			int problemId = loadInt();
			IPath pattern = loadPath();
			return new BuildpathAccessRule(pattern.toString().toCharArray(),
					problemId);
		}

		private IAccessRule[] loadAccessRules() throws IOException {
			int count = loadInt();
			if (count == 0)
				return BuildpathEntry.NO_ACCESS_RULES;
			IAccessRule[] rules = new IAccessRule[count];
			for (int i = 0; i < count; ++i)
				rules[i] = loadAccessRule();
			return rules;
		}

		private IBuildpathAttribute loadAttribute() throws IOException {
			String name = loadString();
			String value = loadString();
			return new BuildpathAttribute(name, value);
		}

		private IBuildpathAttribute[] loadAttributes() throws IOException {
			int count = loadInt();
			if (count == 0)
				return BuildpathEntry.NO_EXTRA_ATTRIBUTES;
			IBuildpathAttribute[] attributes = new IBuildpathAttribute[count];
			for (int i = 0; i < count; ++i)
				attributes[i] = loadAttribute();
			return attributes;
		}

		private boolean loadBoolean() throws IOException {
			return this.in.readBoolean();
		}

		private IBuildpathEntry[] loadBuildpathEntries() throws IOException {
			int count = loadInt();
			IBuildpathEntry[] entries = new IBuildpathEntry[count];
			for (int i = 0; i < count; ++i)
				entries[i] = loadBuildpathEntry();
			return entries;
		}

		private IBuildpathEntry loadBuildpathEntry() throws IOException {
			int id = loadInt();
			if (id < 0 || id > this.allBuildpathEntryCount)
				throw new IOException("Unexpected buildpathentry id"); //$NON-NLS-1$
			if (id < this.allBuildpathEntryCount)
				return this.allBuildpathEntries[id];
			int contentKind = loadInt();
			int entryKind = loadInt();
			IPath path = loadPath();
			IPath[] inclusionPatterns = loadPaths();
			IPath[] exclusionPatterns = loadPaths();
			boolean isExported = loadBoolean();
			boolean isExternal = loadBoolean();
			IAccessRule[] accessRules = loadAccessRules();
			boolean combineAccessRules = loadBoolean();
			IBuildpathAttribute[] extraAttributes = loadAttributes();
			IBuildpathEntry entry = new BuildpathEntry(contentKind, entryKind,
					path, isExported, inclusionPatterns, exclusionPatterns,
					accessRules, combineAccessRules, extraAttributes,
					isExternal);
			IBuildpathEntry[] array = this.allBuildpathEntries;
			if (array == null || id == array.length) {
				array = new IBuildpathEntry[id + ARRAY_INCREMENT];
				if (id != 0)
					System.arraycopy(this.allBuildpathEntries, 0, array, 0, id);
				this.allBuildpathEntries = array;
			}
			array[id] = entry;
			this.allBuildpathEntryCount = id + 1;
			return entry;
		}

		private void loadContainers(IScriptProject project) throws IOException {
			boolean projectIsAccessible = project.getProject().isAccessible();
			int count = loadInt();
			for (int i = 0; i < count; ++i) {
				IPath path = loadPath();
				IBuildpathEntry[] entries = loadBuildpathEntries();
				if (!projectIsAccessible)
					// avoid leaking deleted project's persisted container,
					// but still read the container as it is is part of the file
					// format
					continue;
				IBuildpathContainer container = new PersistedBuildpathContainer(
						project, path, entries);
				ModelManager.this.containerPut(project, path, container);
				Map oldContainers = (Map) ModelManager.this.previousSessionContainers
						.get(project);
				if (oldContainers == null) {
					oldContainers = new HashMap();
					ModelManager.this.previousSessionContainers.put(project,
							oldContainers);
				}
				oldContainers.put(path, container);
			}
		}

		private int loadInt() throws IOException {
			return this.in.readInt();
		}

		private IPath loadPath() throws IOException {
			if (loadBoolean())
				return null;
			String portableString = loadString();
			IPath path = (IPath) this.allPaths.get(portableString);
			if (path == null) {
				path = Path.fromPortableString(portableString);
				this.allPaths.put(portableString, path);
			}
			return path;
		}

		private IPath[] loadPaths() throws IOException {
			int count = loadInt();
			IPath[] pathArray = new IPath[count];
			for (int i = 0; i < count; ++i)
				pathArray[i] = loadPath();
			return pathArray;
		}

		private void loadProjects(IScriptModel model) throws IOException {
			int count = loadInt();
			for (int i = 0; i < count; ++i) {
				String projectName = loadString();
				loadContainers(model.getScriptProject(projectName));
			}
		}

		private String loadString() throws IOException {
			int id = loadInt();
			if (id < 0 || id > this.allStringsCount)
				throw new IOException("Unexpected string id"); //$NON-NLS-1$
			if (id < this.allStringsCount)
				return this.allStrings[id];
			String string = this.in.readUTF();
			String[] array = this.allStrings;
			if (array == null || id == array.length) {
				array = new String[id + ARRAY_INCREMENT];
				if (id != 0)
					System.arraycopy(this.allStrings, 0, array, 0, id);
				this.allStrings = array;
			}
			array[id] = string;
			this.allStringsCount = id + 1;
			return string;
		}
	}

	/**
	 * Returns the name of the container IDs for which an CP container
	 * initializer is registered through an extension point
	 */
	public static String[] getRegisteredContainerIDs() {

		Plugin dltkCorePlugin = DLTKCore.getPlugin();
		if (dltkCorePlugin == null)
			return null;

		ArrayList containerIDList = new ArrayList(5);
		IExtensionPoint extension = Platform.getExtensionRegistry()
				.getExtensionPoint(DLTKCore.PLUGIN_ID,
						ModelManager.BPCONTAINER_INITIALIZER_EXTPOINT_ID);
		if (extension != null) {
			IExtension[] extensions = extension.getExtensions();
			for (int i = 0; i < extensions.length; i++) {
				IConfigurationElement[] configElements = extensions[i]
						.getConfigurationElements();
				for (int j = 0; j < configElements.length; j++) {
					String idAttribute = configElements[j].getAttribute("id"); //$NON-NLS-1$
					if (idAttribute != null)
						containerIDList.add(idAttribute);
				}
			}
		}
		String[] containerIDs = new String[containerIDList.size()];
		containerIDList.toArray(containerIDs);
		return containerIDs;
	}

	private synchronized void containersReset(String[] containerIDs) {
		for (int i = 0; i < containerIDs.length; i++) {
			String containerID = containerIDs[i];
			Iterator projectIterator = this.containers.keySet().iterator();
			while (projectIterator.hasNext()) {
				IScriptProject project = (IScriptProject) projectIterator
						.next();
				Map projectContainers = (Map) this.containers.get(project);
				if (projectContainers != null) {
					Iterator containerIterator = projectContainers.keySet()
							.iterator();
					while (containerIterator.hasNext()) {
						IPath containerPath = (IPath) containerIterator.next();
						if (containerPath.segment(0).equals(containerID)) { // registered
							// container
							projectContainers.put(containerPath, null); // reset
							// container
							// value,
							// but
							// leave
							// entry
							// in
							// Map
						}
					}
				}
			}
		}
	}

	/**
	 * Flushes ZipFiles cache if there are no more clients.
	 */
	public void flushZipFiles() {
		Thread currentThread = Thread.currentThread();
		HashMap map = (HashMap) this.zipFiles.get();
		if (map == null)
			return;
		this.zipFiles.set(null);
		Iterator iterator = map.values().iterator();
		while (iterator.hasNext()) {
			try {
				ZipFile zipFile = (ZipFile) iterator.next();
				if (ModelManager.ZIP_ACCESS_VERBOSE) {
					System.out
							.println("(" + currentThread + ") [ModelManager.flushZipFiles()] Closing ZipFile on " + zipFile.getName()); //$NON-NLS-1$//$NON-NLS-2$
				}
				zipFile.close();
			} catch (IOException e) {
				// problem occured closing zip file: cannot do much more
			}
		}
	}

	private SourceModuleInfoCache sourceModuleInfoCach;

	public ISourceModuleInfoCache getSourceModuleInfoCache() {
		if (sourceModuleInfoCach == null) {
			sourceModuleInfoCach = new SourceModuleInfoCache();
		}
		return sourceModuleInfoCach;
	}

	public static UserLibraryManager getUserLibraryManager() {
		if (MANAGER.userLibraryManager == null) {
			UserLibraryManager libraryManager = new UserLibraryManager();
			synchronized (MANAGER) {
				if (MANAGER.userLibraryManager == null) { // ensure another
					// library manager
					// was not set while
					// creating the
					// instance above
					MANAGER.userLibraryManager = libraryManager;
				}
			}
		}
		return MANAGER.userLibraryManager;
	}
}
