/*
* Copyright 2000-2013 JetBrains s.r.o.
* Copyright 2014-2014 AS3Boyan
* Copyright 2014-2014 Elias Ku
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.plugins.haxe.haxelib;
import com.intellij.compiler.ant.BuildProperties;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.plugins.haxe.hxml.HXMLFileType;
import com.intellij.plugins.haxe.hxml.psi.HXMLClasspath;
import com.intellij.plugins.haxe.hxml.psi.HXMLLib;
import com.intellij.plugins.haxe.ide.module.HaxeModuleSettings;
import com.intellij.plugins.haxe.ide.module.HaxeModuleType;
import com.intellij.plugins.haxe.nmml.NMMLFileType;
import com.intellij.plugins.haxe.util.HaxeDebugTimeLog;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import org.apache.log4j.Level;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.io.LocalFileFinder;
import java.io.File;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Manages Haxe library class paths across projects.
*
* This class is intended to keep the class paths up to date as the projects
* and module settings change. It encapsulates reading classpaths from the
* various types of Haxe project definitions (OpenFL, NME, etc.) and adding
* them to module settings so that the paths available at runtime are also
* available when writing the code.
*
*
* Implementation Note: We might need to track each module separately, in a
* list attached to the project. We'd need that in case we can update the
* project fast enough that all of the modules haven't been opened yet.
* However, this is unlikely, since the first project update run takes place
* after the project has finished initializing, and the open notifications
* come during initialization.
*
*/
public class HaxelibProjectUpdater {
static Logger LOG = Logger.getInstance("#com.intellij.plugins.haxe.haxelib.HaxelibManager");
{
LOG.setLevel(Level.DEBUG);
}
/**
* Set this to run in the foreground for speed testing.
* It overrides myRunInForeground. The UI is blocked with no updates.
*/
private static final boolean myTestInForeground = false;
/**
* Set this true to put up a modal dialog and run in the foreground thread
* (locking up the UI.)
* Set it false to run in a background thread. Progress is updated in the
* status bar and the UI is usable.
*/
private static final boolean myRunInForeground = false;
public static final HaxelibProjectUpdater INSTANCE = new HaxelibProjectUpdater();
private ProjectUpdateQueue myQueue = null;
private ProjectMap myProjects = null;
private HaxelibProjectUpdater() {
myQueue = new ProjectUpdateQueue();
myProjects = new ProjectMap();
}
@NotNull
static HaxelibProjectUpdater getInstance() {
return INSTANCE;
}
/**
* Adds a new project to the manager. This is normally called in response to a
* ModuleComponent.openProject() notification. Multiple modules referencing
* the same project cause a counter to be incremented.
*
* @param project that is being opened.
*/
public void openProject(@NotNull Project project) {
ProjectTracker tracker = myProjects.add(project);
tracker.setDirty(true);
myQueue.add(tracker);
}
/**
* Close and possibly remove a project, if the reference count has been exhausted.
*
* @param project to close
* @return whether the close has been delayed because an update is in process.
*/
public boolean closeProject(Project project) {
boolean delayed = false;
boolean removed = false;
ProjectTracker tracker = myProjects.get(project);
removed = myProjects.remove(project);
if (removed) {
myQueue.remove(tracker);
if (tracker.equals(myQueue.getUpdatingProject())) {
delayed = true;
}
}
return delayed;
}
/**
* Retrieve the HaxelibLibraryCacheManager for a given module/project.
*
* Convenience function that doesn't quite match the purpose of this class,
* but we haven't made the CacheManager a singleton -- and we really can't
* unless we move the notion of a project into it.
*
* @param module that we need the HaxelibLibraryCacheManager for.
* @return the appropriate HaxelibLibraryCacheManager.
*/
@Nullable
public HaxelibLibraryCacheManager getLibraryCacheManager(@NotNull Module module) {
return getLibraryCacheManager(module.getProject());
}
/**
* Retrieve the HaxelibLibraryCacheManager for a given module/project.
*
* Convenience function that doesn't quite match the purpose of this class,
* but we haven't made the CacheManager a singleton -- and we really can't
* unless we move the notion of a project into it.
*
* @param project that we need the HaxelibLibraryCacheManager for.
* @return the appropriate HaxelibLibraryCacheManager.
*/
@Nullable
public HaxelibLibraryCacheManager getLibraryCacheManager(@NotNull Project project) {
ProjectTracker tracker = myProjects.get(project);
return null == tracker ? null : tracker.getSdkManager();
}
/**
* Resolve the classpath/library entries for a module. Determines which
* libraries to add and remove from the module. Only libraries that have
* previously been added may be removed, if they have become redundant
* or otherwise specified.
*
* @param tracker for the project being updated.
* @param module being updated.
* @param externalClasspaths potential new classpaths that must be available
* to the module when this routine finishes.
*/
private void resolveModuleLibraries(ProjectTracker tracker, Module module, HaxeClasspath externalClasspaths) {
HaxeClasspath toAdd;
HaxeClasspath toRemove;
ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
// Remove project level classpath items from the list of required
// external libraries.
//
// If the SDK is inherited, then we can use the project dependencies to
// filter against. Otherwise, the project dependencies may not be
// reliable, since they are not made using the same haxelib. Checking
// whether they resolve to the same thing may be difficult, given that
// the actual library location, etc. is kept in the external .xml files,
// or *may* be in the environment (though our environment is constant).
//
// For the moment, we'll presume that if the SDK is NOT inherited, the
// module gets the full list.
//
// We're also checking whether the classpath is known to the SDK. If so,
// we also want to filter it out.
//
HaxeClasspath inheritedClasspaths = HaxelibClasspathUtils.getSdkClasspath(
HaxelibSdkUtils.lookupSdk(module));
if (rootManager.isSdkInherited()) {
// Add project classpaths before SDK class paths for correct precedence.
HaxeClasspath projectClasspaths = getProjectClasspath(tracker);
projectClasspaths.addAll(inheritedClasspaths);
inheritedClasspaths = projectClasspaths;
}
class NewPathCollector implements HaxeClasspath.Lambda {
public HaxeClasspath myUninherited = new HaxeClasspath();
private HaxeClasspath myInherited;
public NewPathCollector(HaxeClasspath inherited) { myInherited = inherited; }
@Override
public boolean processEntry(HaxeClasspathEntry externalPath) {
if (!myInherited.contains(externalPath)) {
myUninherited.add(externalPath);
}
return true;
}
}
NewPathCollector npCollector = new NewPathCollector(inheritedClasspaths);
externalClasspaths.iterate(npCollector);
HaxeClasspath uninheritedExternalClasspaths = npCollector.myUninherited;
// Figure out which libs from the module to remove. To be candidates for
// removal:
// - First, they must to be managed libraries/paths.
// - Second, they must not appear in the uninherited external classpaths, OR
// they appear in the project or SDK classpaths from the module.
class RemoveCollector implements HaxeClasspath.Lambda {
HaxeClasspath uninherited;
HaxeClasspath inherited;
public HaxeClasspath toRemove = new HaxeClasspath();
public RemoveCollector(HaxeClasspath uninheritedClasspath, HaxeClasspath inheritedClasspath) {
uninherited = uninheritedClasspath;
inherited = inheritedClasspath;
}
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
if (entry.isManagedEntry()
&& (!uninherited.contains(entry) || inherited.contains(entry))) {
toRemove.add(entry);
}
return true;
}
}
RemoveCollector collector = new RemoveCollector(uninheritedExternalClasspaths, inheritedClasspaths);
HaxeClasspath moduleClasspath = HaxelibClasspathUtils.getModuleClasspath(module);
moduleClasspath.iterate(collector);
toRemove = collector.toRemove;
// uninheritecExternalClaspaths should not contain any non-haxelib entries,
// so we don't have to worry about that check here.
toAdd = uninheritedExternalClasspaths;
toAdd.removeAll(moduleClasspath);
// Anything new must be marked as managed.
toAdd.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
entry.markAsManagedEntry();
return true;
}
});
updateModule(module, toRemove, toAdd);
}
/**
* Workhorse routine for resolveModuleLibraries. This does the actual
* update of the module. It will block until all of the running events
* on the AWT thread have completed, and then this will run on that thread.
*
* @param module to update.
* @param toRemove libraries that need to be removed from the module.
* @param toAdd libraries that need to be added to the module.
*/
private void updateModule(final Module module, final HaxeClasspath toRemove, final HaxeClasspath toAdd) {
if ((null == toRemove || toRemove.isEmpty()) && (null == toAdd || toAdd.isEmpty())) {
return;
}
if (null != toRemove) {
toRemove.iterate( new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
LOG.assertTrue(entry.isManagedEntry(), "Attempting to automatically remove a library that was not marked as managed.");
return true;
}
});
}
if (null != toAdd) {
toAdd.iterate( new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
LOG.assertTrue(entry.isManagedEntry(), "Attempting to automatically add a library that is not marked as managed.");
return true;
}
});
}
final HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("Write action:");
timeLog.stamp("Queueing write action...");
doWriteAction(new Runnable() {
@Override
public void run() {
timeLog.stamp("<-- Time elapsed waiting for write access on the AWT thread.");
timeLog.stamp("Begin: Updating module libraries for " + module.getName());
ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
ModifiableRootModel modifiableModel = rootManager.getModifiableModel();
final LibraryTable libraryTable = modifiableModel.getModuleLibraryTable();
// Remove unused packed "haxelib|<lib_name>" libraries from the module and project library.
if (null != toRemove) {
timeLog.stamp("Removing libraries.");
toRemove.iterate(new HaxeClasspath.Lambda(){
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
Library library = libraryTable.getLibraryByName(entry.getName());
if (null != library) {
// Why use this?: ModuleHelper.removeDependency(rootManager, library);
libraryTable.removeLibrary(library);
timeLog.stamp("Removed library " + library.getName());
}
else {
LOG.warn(
"Internal inconsistency: library to remove was not found: " +
entry.getName());
}
return true;
}
});
}
// Add new dependencies to modules.
if (null != toAdd) {
timeLog.stamp("Locating libraries and adding dependencies.");
toAdd.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
Library libraryByName = libraryTable.getLibraryByName(
entry.getName());
if (libraryByName == null) {
libraryByName = libraryTable.createLibrary(entry.getName());
Library.ModifiableModel libraryModifiableModel = libraryByName.getModifiableModel();
libraryModifiableModel.addRoot(entry.getUrl(), OrderRootType.CLASSES);
libraryModifiableModel.addRoot(entry.getUrl(), OrderRootType.SOURCES);
libraryModifiableModel.commit();
timeLog.stamp("Added library " + libraryByName.getName());
}
else {
LOG.warn("Internal inconsistency: library to add was already in the module's library table.");
}
return true;
}
});
}
timeLog.stamp("Committing changes to module libraries");
modifiableModel.commit();
timeLog.stamp("Finished: Updating module Libraries");
}
});
timeLog.print();
}
/**
* The guts of syncModuleClasspaths, separated so that it can be run as
* either a foreground or background task.
*
* @param tracker for the project being updated.
* @param module being updated.
* @param timeLog where to log timing results
*/
private void syncOneModule(@NotNull final ProjectTracker tracker, @NotNull Module module, @NotNull HaxeDebugTimeLog timeLog) {
Project project = tracker.getProject();
HaxeClasspath haxelibExternalItems = new HaxeClasspath();
HaxeClasspath haxelibNewItemList;
HaxelibLibraryCache libManager = tracker.getSdkManager().getLibraryManager(module);
HaxeModuleSettings settings = HaxeModuleSettings.getInstance(module);
int buildConfig = settings.getBuildConfig();
switch (buildConfig) {
case HaxeModuleSettings.USE_NMML:
timeLog.stamp("Start loading classpaths from NMML file.");
haxelibNewItemList = libManager.findHaxelibPath("nme");
haxelibExternalItems.addAll(haxelibNewItemList);
String nmmlPath = settings.getNmmlPath();
if (nmmlPath != null && !nmmlPath.isEmpty()) {
VirtualFile file = LocalFileFinder.findFile(nmmlPath);
if (file != null && file.getFileType().equals(NMMLFileType.INSTANCE)) {
VirtualFileManager.getInstance().syncRefresh();
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile != null && psiFile instanceof XmlFile) {
haxelibExternalItems.addAll(HaxelibClasspathUtils.getHaxelibsFromXmlFile((XmlFile)psiFile, libManager));
}
}
}
timeLog.stamp("Finished loading classpaths from NMML file.");
break;
case HaxeModuleSettings.USE_OPENFL:
timeLog.stamp("Start loading classpaths from openfl file.");
haxelibNewItemList = libManager.findHaxelibPath("openfl");
haxelibExternalItems.addAll(haxelibNewItemList);
String openFLXmlPath = settings.getOpenFLPath();
if (openFLXmlPath != null && !openFLXmlPath.isEmpty()) {
VirtualFile file = LocalFileFinder.findFile(openFLXmlPath);
if (file != null && file.getFileType().equals(XmlFileType.INSTANCE)) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile != null && psiFile instanceof XmlFile) {
haxelibExternalItems.addAll(HaxelibClasspathUtils.getHaxelibsFromXmlFile((XmlFile)psiFile, libManager));
}
}
}
else {
File dir = BuildProperties.getProjectBaseDir(project);
List<String> projectClasspaths =
HaxelibClasspathUtils.getProjectDisplayInformation(project, dir, "openfl",
HaxelibSdkUtils.lookupSdk(module));
for (String classpath : projectClasspaths) {
VirtualFile file = LocalFileFinder.findFile(classpath);
if (file != null) {
haxelibExternalItems.add(new HaxelibItem(classpath, file.getUrl()));
}
}
}
haxelibExternalItems.debugDump("haxelibExternalItems for module "+ module.getName());
timeLog.stamp("Finished loading classpaths from openfl file.");
break;
case HaxeModuleSettings.USE_HXML:
timeLog.stamp("Start loading classpaths from HXML file.");
String hxmlPath = settings.getHxmlPath();
if (hxmlPath != null && !hxmlPath.isEmpty()) {
VirtualFile file = LocalFileFinder.findFile(hxmlPath);
if (file != null && file.getFileType().equals(HXMLFileType.INSTANCE)) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile != null) {
Collection<HXMLClasspath> hxmlClasspaths = PsiTreeUtil.findChildrenOfType(psiFile, HXMLClasspath.class);
for (HXMLClasspath hxmlClasspath : hxmlClasspaths) {
String classpath = hxmlClasspath.getValue();
haxelibExternalItems.add(new HaxelibItem(classpath, VfsUtil.pathToUrl(classpath)));
}
Collection<HXMLLib> hxmlLibs = PsiTreeUtil.findChildrenOfType(psiFile, HXMLLib.class);
for (HXMLLib hxmlLib : hxmlLibs) {
String name = hxmlLib.getValue();
haxelibNewItemList = libManager.findHaxelibPath(name);
haxelibExternalItems.addAll(haxelibNewItemList);
}
}
}
}
timeLog.stamp("Finish loading classpaths from HXML file.");
break;
case HaxeModuleSettings.USE_PROPERTIES:
timeLog.stamp("Start loading classpaths from properties.");
String arguments = settings.getArguments();
if (!arguments.isEmpty()) {
List<String> classpaths = HaxelibClasspathUtils.getHXMLFileClasspaths(project, arguments);
for (String classpath : classpaths) {
haxelibExternalItems.add(new HaxelibItem(classpath, VfsUtil.pathToUrl(classpath)));
}
}
timeLog.stamp("Finish loading classpaths from properties.");
break;
}
// We can't just remove all of the project classpaths from the module's
// library list here because we need to remove any managed classpaths that
// are no longer valid in the modules. We can't do that if we don't have
// the list of valid ones. :/
timeLog.stamp("Adding libraries to module.");
resolveModuleLibraries(tracker, module, haxelibExternalItems);
timeLog.stamp("Finished adding libraries to module.");
}
private void syncModuleClasspaths(final ProjectTracker tracker) {
final HaxeDebugTimeLog timeLog = HaxeDebugTimeLog.startNew("syncModuleClasspaths");
final Project project = tracker.getProject();
//LOG.debug("Scanning project " + project.getName());
timeLog.stamp("Scanning project " + project.getName());
Collection<Module> modules = ModuleUtil.getModulesOfType(project, HaxeModuleType.getInstance());
int i = 0;
final int count = modules.size();
for (final Module module : modules) {
final int num = ++i;
//LOG.debug("Scanning module " + (++i) + " of " + count + ": " + module.getName());
timeLog.stamp("\nScanning module " + (num) + " of " + count + ": " + module.getName());
if (myTestInForeground) {
syncOneModule(tracker, module, timeLog);
} else {
// Running inside of a read action lets the UI run, and messes with the timing.
doReadAction(new Runnable() { @Override public void run() {
syncOneModule(tracker, module, timeLog);
} });
}
}
timeLog.stamp("Completed.");
timeLog.print();
}
private void synchronizeClasspaths(@NotNull ProjectTracker tracker) {
syncProjectClasspath(tracker);
syncModuleClasspaths(tracker);
}
/**
* Retrieves the project's classpath, either from the cache if available,
* or from the project's library table.
*
* @param tracker for the project being updated.
* @return a HaxeClasspath representing the libraries specified at the project level.
*/
@NotNull
private HaxeClasspath getProjectClasspath(@NotNull ProjectTracker tracker) {
ProjectClasspathCache cache = tracker.getCache();
HaxeClasspath projectClasspath;
int buildConfig = HaxeModuleSettings.USE_PROPERTIES; // Only properties available.
if (cache.isClasspathSetFor(buildConfig)) {
projectClasspath = cache.getClasspathFor(buildConfig);
} else {
projectClasspath = HaxelibClasspathUtils.getProjectLibraryClasspath(
tracker.getProject());
cache.setClasspathFor(buildConfig, projectClasspath);
}
return projectClasspath;
}
/**
* Removes old unneeded libraries and adds new dependencies to the project classpath.
* Queues an update to the Project.
*
* @param tracker for the project being updated.
*/
@NotNull
private void syncProjectClasspath(@NotNull ProjectTracker tracker) {
HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("syncProjectClasspath");
timeLog.stamp("Start synchronizing project " + tracker.getProject().getName());
Sdk sdk = HaxelibSdkUtils.lookupSdk(tracker.getProject());
HaxelibLibraryCache libCache = tracker.getSdkManager().getLibraryCache(sdk);
HaxeClasspath currentProjectClasspath = HaxelibClasspathUtils.getProjectLibraryClasspath(
tracker.getProject());
List<String> currentLibraryNames = HaxelibClasspathUtils.getProjectLibraryNames(tracker.getProject(), true);
HaxeClasspath haxelibClasspaths = libCache.getClasspathForHaxelibs(currentLibraryNames);
// Libraries that we want to remove are those specified as 'haxelib' entries and are
// no longer referenced.
class Collector implements HaxeClasspath.Lambda {
public HaxeClasspath toRemove = new HaxeClasspath();
HaxeClasspath myFilter;
public Collector(HaxeClasspath filter) { myFilter = filter; }
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
if (entry.isManagedEntry() && !myFilter.contains(entry)) {
toRemove.add(entry);
}
return true;
}
}
Collector collector = new Collector(haxelibClasspaths);
currentProjectClasspath.iterate(collector);
HaxeClasspath toRemove = collector.toRemove;
// Libraries that we want to add are those that aren't already on the current classpath.
haxelibClasspaths.removeAll(currentProjectClasspath);
HaxeClasspath toAdd = haxelibClasspaths;
if (!toAdd.isEmpty() && !toRemove.isEmpty()) {
timeLog.stamp("Add/Remove calculations finished. Queuing write task.");
updateProject(tracker, toRemove, toAdd);
}
timeLog.stamp("Finished synchronizing.");
timeLog.print();
// And update the cache.
currentProjectClasspath.removeAll(toRemove);
currentProjectClasspath.addAll(toAdd);
tracker.getCache().setPropertiesClassPath(currentProjectClasspath);
}
/**
* Workhorse routine for syncProjectClasspath. This does the actual update of the
* project. It will block until all of the running events on the AWT thread have
* completed, and then this will run on that thread.
*
* @param tracker for the project to update.
* @param toRemove libraries that need to be removed from the project.
* @param toAdd libraries that need to be added to the project.
*/
private void updateProject(@NotNull final ProjectTracker tracker, final @Nullable HaxeClasspath toRemove, final @Nullable HaxeClasspath toAdd) {
if (null == toRemove && null == toAdd) {
return;
}
if (null != toRemove) {
toRemove.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
LOG.assertTrue(entry.isManagedEntry(), "Attempting to automatically remove a library that was not marked as managed.");
return true;
}
});
}
if (null != toAdd) {
toAdd.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
LOG.assertTrue(entry.isManagedEntry(), "Attempting to automatically add a library that is not marked as managed.");
return true;
}
});
}
doWriteAction(new Runnable() {
@Override
public void run() {
final HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("Write action:");
timeLog.stamp("Begin: Updating project libraries");
LibraryTable projectTable = ProjectLibraryTable.getInstance(tracker.getProject());
final LibraryTable.ModifiableModel projectModifiableModel = projectTable.getModifiableModel();
// Remove unused packed "haxelib|<lib_name>" libraries from the module and project library.
if (null != toRemove) {
timeLog.stamp("Removing unneeded haxelib libraries.");
toRemove.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry entry) {
Library library = projectModifiableModel.getLibraryByName(
entry.getName());
LOG.assertTrue(null != library, "Library " + entry.getName() + " was not found in the project and will not be removed.");
if (null != library) {
projectModifiableModel.removeLibrary(library);
timeLog.stamp("Removed library " + entry.getName());
}
else {
timeLog.stamp(
"Library to remove was not found: " + entry.getName());
}
return true;
}
});
}
// Add new dependencies to modules.
if (null != toAdd) {
timeLog.stamp("Adding haxelib dependencies.");
toAdd.iterate(new HaxeClasspath.Lambda() {
@Override
public boolean processEntry(HaxeClasspathEntry newItem) {
Library libraryByName = projectModifiableModel.getLibraryByName(newItem.getName());
if (libraryByName == null) {
libraryByName = projectModifiableModel.createLibrary(newItem.getName());
Library.ModifiableModel libraryModifiableModel = libraryByName.getModifiableModel();
libraryModifiableModel.addRoot(newItem.getUrl(), OrderRootType.CLASSES);
libraryModifiableModel.addRoot(newItem.getUrl(), OrderRootType.SOURCES);
libraryModifiableModel.commit();
timeLog.stamp("Added library " + libraryByName.getName());
}
return true;
}
});
}
timeLog.stamp("Committing project changes.");
projectModifiableModel.commit();
timeLog.stamp("Finished: Updating project Libraries");
timeLog.print();
}
});
}
/**
* Cause a synchronous write action to be run on the AWT thread.
*
* @param action action to run.
*/
private static void doWriteAction(final Runnable action) {
final Application application = ApplicationManager.getApplication();
application.invokeAndWait(new Runnable() {
@Override
public void run() {
application.runWriteAction(action);
}
}, application.getDefaultModalityState());
}
/**
* Cause a synchronous read action to be run. Blocks if a write action is
* currently running in the AWT thread. Also blocks write actions from
* occuring while this is being run. So don't let tasks take too long, or
* the UI gets choppy.
*
* @param action action to run.
*/
private static void doReadAction(final Runnable action) {
final Application application = ApplicationManager.getApplication();
application.invokeAndWait(new Runnable() {
@Override
public void run() {
application.runReadAction(action);
}
}, application.getDefaultModalityState());
}
/**
* Cache for project library classpaths.
*/
final private class ProjectClasspathCache {
private HaxeClasspath nmmlClassPath;
private HaxeClasspath openFLClassPath;
private HaxeClasspath hxmlClassPath;
private HaxeClasspath propertiesClassPath;
private boolean nmmlIsSet;
private boolean openFLIsSet;
private boolean hxmlIsSet;
private boolean propertiesIsSet;
public ProjectClasspathCache() {
clear();
}
public void clear() {
setNmmlClassPath(HaxeClasspath.EMPTY_CLASSPATH);
setOpenFLClassPath(HaxeClasspath.EMPTY_CLASSPATH);
setHxmlClassPath(HaxeClasspath.EMPTY_CLASSPATH);
setPropertiesClassPath(HaxeClasspath.EMPTY_CLASSPATH);
// Reset the 'set' bits.
nmmlIsSet = openFLIsSet = hxmlIsSet = propertiesIsSet = false;
}
public boolean isClasspathSetFor(int buildConfig) {
switch(buildConfig) {
case HaxeModuleSettings.USE_NMML:
return nmmlIsSet;
case HaxeModuleSettings.USE_OPENFL:
return openFLIsSet;
case HaxeModuleSettings.USE_HXML:
return hxmlIsSet;
case HaxeModuleSettings.USE_PROPERTIES:
return propertiesIsSet;
}
return false;
}
@NotNull
public HaxeClasspath getClasspathFor(int buildConfig) {
switch(buildConfig) {
case HaxeModuleSettings.USE_NMML:
return getNmmlClassPath();
case HaxeModuleSettings.USE_OPENFL:
return getOpenFLClassPath();
case HaxeModuleSettings.USE_HXML:
return getHxmlClassPath();
case HaxeModuleSettings.USE_PROPERTIES:
return getPropertiesClassPath();
}
return HaxeClasspath.EMPTY_CLASSPATH;
}
public void setClasspathFor(int buildConfig, HaxeClasspath classpath) {
switch(buildConfig) {
case HaxeModuleSettings.USE_NMML:
setNmmlClassPath(classpath);
case HaxeModuleSettings.USE_OPENFL:
setOpenFLClassPath(classpath);
case HaxeModuleSettings.USE_HXML:
setHxmlClassPath(classpath);
case HaxeModuleSettings.USE_PROPERTIES:
setPropertiesClassPath(classpath);
}
}
@NotNull
public HaxeClasspath getNmmlClassPath() {
return nmmlClassPath != null ? nmmlClassPath : HaxeClasspath.EMPTY_CLASSPATH;
}
@NotNull
public HaxeClasspath getOpenFLClassPath() {
return openFLClassPath != null ? openFLClassPath : HaxeClasspath.EMPTY_CLASSPATH;
}
@NotNull
public HaxeClasspath getHxmlClassPath() {
return hxmlClassPath != null ? hxmlClassPath : HaxeClasspath.EMPTY_CLASSPATH;
}
@NotNull
public HaxeClasspath getPropertiesClassPath() {
return propertiesClassPath != null ? propertiesClassPath : HaxeClasspath.EMPTY_CLASSPATH;
}
public void setNmmlClassPath(HaxeClasspath nmmlClasspath) {
nmmlClassPath = nmmlClasspath;
nmmlIsSet = true;
}
public void setOpenFLClassPath(HaxeClasspath openFLClassPath) {
this.openFLClassPath = openFLClassPath;
openFLIsSet = true;
}
public void setHxmlClassPath(HaxeClasspath hxmlClassPath) {
this.hxmlClassPath = hxmlClassPath;
hxmlIsSet = true;
}
public void setPropertiesClassPath(HaxeClasspath propertiesClassPath) {
this.propertiesClassPath = propertiesClassPath;
propertiesIsSet = true;
}
}
/**
* Tracks the state of a project for updating class paths.
*/
public final class ProjectTracker {
final Project myProject;
boolean myIsDirty;
boolean myIsUpdating;
ProjectClasspathCache myCache;
HaxelibLibraryCacheManager mySdkManager;
// TODO: Determine if we need to track whether the project is still open.
/**
* Typically, a project gets open and closed events for all of the modules it
* contains. We don't want to destroy or de-queue ProjectTrackers until all
* of the modules have been destroyed.
*/
int myReferenceCount;
public ProjectTracker(Project project) {
myProject = project;
myIsDirty = true;
myIsUpdating = false;
myReferenceCount = 0;
myCache = new ProjectClasspathCache();
mySdkManager = new HaxelibLibraryCacheManager();
}
/**
* Get the settings cache.
*/
@NotNull
public ProjectClasspathCache getCache() {
return myCache;
}
/**
* Get the library classpath cache.
*/
@NotNull
public HaxelibLibraryCacheManager getSdkManager() {
return mySdkManager;
}
/**
* Tell whether this project is dirty (needs updating).
*
* @return true if the project needs updating.
*/
public boolean isDirty() {
boolean ret = false;
synchronized (this) {
ret = myIsDirty;
}
return ret;
}
/**
* Set/clear the dirty state. Marking the project dirty clears the cache.
*
* @param newState the new state to set.
* @return the state prior to setting it.
*/
public boolean setDirty(boolean newState) {
boolean ret = false;
synchronized (this) {
ret = myIsDirty;
myIsDirty = newState;
if (myIsDirty) {
// XXX: May need something more sophisicated than just clearing it.
// It could be that a module is getting changed, but not
// the project. In that case, we would want to detect whether
// the project settings really changed, and act accordingly.
myCache.clear();
}
}
return ret;
}
/**
* Tell whether this project is currently updating.
*
* @return true if the project is currently running an update.
*/
public boolean isUpdating() {
boolean ret = false;
synchronized(this) {
ret = myIsUpdating;
}
return ret;
}
/**
* Set/clear the 'updating' state.
*
* @param newState the new state to set.
* @return the state prior to setting it.
*/
public boolean setUpdating(boolean newState) {
boolean ret = false;
synchronized(this) {
ret = myIsUpdating;
myIsUpdating = newState;
}
return ret;
}
/**
* Increase the reference count.
*/
public void addReference() {
synchronized(this) {
myReferenceCount++;
}
}
/**
* Decrease the reference count.
*
* @return the number of references still attached to the object.
*/
public int removeReference() {
int refs;
synchronized(this) {
refs = --myReferenceCount;
}
// XXX: Maybe we don't need an assertion for this.
LOG.assertTrue(refs >= 0);
return refs;
}
/**
* Get the project we are tracking. Note that the project may
* not still be open at the moment that this is retrieved.
*/
@NotNull
public Project getProject() {
return myProject;
}
@NotNull
public String toString() {
return "ProjectTracker:" + myProject.getName();
}
public boolean equalsName(@Nullable ProjectTracker tracker) {
if (null == tracker) {
return false;
}
return myProject.getName().equals(tracker.getProject().getName());
}
} // end class ProjectTracker
/**
* Tracks all of the projects that are currently open. (As opposed to those
* that are bing queued for update, which the ProjectUpdateQueue does.)
* ProjectTrackers are shared between this map and the queue.
*/
public final class ProjectMap {
// Hashtable is already synchronized, so we don't have to manage that ourselves.
final Hashtable<String, ProjectTracker> myMap;
public ProjectMap() {
myMap = new Hashtable<String, ProjectTracker>();
}
/**
* Adds a new project to the map, if it doesn't exist there already.
*
* @param project An open project to be tracked.
*
* @return a new ProjectTracker for the project added, or the existing
* ProjectTracker for an existing project.
*/
@NotNull
public ProjectTracker add(@NotNull Project project) {
ProjectTracker tracker;
synchronized (this) {
tracker = myMap.get(project.getName());
if (null == tracker) {
tracker = new ProjectTracker(project);
myMap.put(project.getName(), tracker);
}
tracker.addReference();
}
return tracker;
}
public boolean remove(@NotNull Project project) {
boolean removed = false;
synchronized(this) {
ProjectTracker tracker = myMap.get(project.getName());
if (null != tracker) {
int refs = tracker.removeReference();
if (refs == 0) {
removed = null != myMap.remove(project.getName());
}
}
}
return removed;
}
@Nullable
public ProjectTracker get(@NotNull Project project) {
ProjectTracker tracker;
synchronized(this) {
tracker = myMap.get(project.getName());
}
return tracker;
}
}
/**
* A FIFO queue for projects that need updating. Projects are tracked
* through the ProjectTracker class. When a project placed is in this queue,
* it is marked dirty. When the project is being updated, it's marked
* as updating. The currently updating project can be retrieved with
* getUpdatingProject().
*/
final class ProjectUpdateQueue {
final Object updateSyncToken;
ConcurrentLinkedQueue<ProjectTracker> queue;
ProjectTracker updatingProject = null;
public ProjectUpdateQueue() {
queue = new ConcurrentLinkedQueue<ProjectTracker>();
updateSyncToken = new Object();
updatingProject = null;
}
/**
* Determine whether there are any projects awaiting updating.
*
* The queue may be empty even though a project is currently updating.
*
* @return whether there are any projects waiting to be updated.
*/
public boolean isEmpty() {
return queue.isEmpty();
}
/**
* Adds a new project to the update queue. If the project already
* exists in the queue (as described by equals()) then it will not
* be added.
*
* @param tracker for the project that needs to be updated.
* @return true if the project was added to the update queue.
*/
public boolean add(@NotNull ProjectTracker tracker) {
boolean ret = false;
// XXX: Something seems wrong about skipping the currently updating project.
// What if a project change happens while the project is already running?
// Should we cancel and restart instead? And, if so, should we have a
// short delay to ensure that all identical messages are already queued?
if (!tracker.equalsName(getUpdatingProject())) {
if (queue.isEmpty() || !queue.contains(tracker)) {
ret = queue.add(tracker);
if (null == getUpdatingProject()) {
queueNextProject();
}
}
}
return ret;
}
/**
* Remove a project from the update queue.
*
* Projects that are currently updating are not canceled or removed, but will
* be as soon as they are finished.
*
* @param tracker project to remove
* @return true if the project was removed; false otherwise, or if it wasn't queued.
*/
public boolean remove(@NotNull ProjectTracker tracker) {
boolean removed = false;
synchronized(updateSyncToken) {
if (queue.remove(tracker)) {
tracker.setUpdating(false);
// We haven't changed anything, so it's still dirty.
removed = true;
}
}
return removed;
}
/**
* @return the project currently being updated, if any.
*/
@Nullable
public ProjectTracker getUpdatingProject() {
ProjectTracker tracker;
synchronized (updateSyncToken) {
tracker = updatingProject;
}
return tracker;
}
/**
* Puts the next project on the event queue.
*/
private void queueNextProject() {
synchronized (updateSyncToken) {
LOG.assertTrue(null == updatingProject);
// Get the next project from the queue. We're done if there's
// nothing left.
updatingProject = queue.poll(); // null if empty.
if (updatingProject == null) return;
LOG.assertTrue(updatingProject.isDirty());
LOG.assertTrue(!updatingProject.isUpdating());
updatingProject.setUpdating(true);
}
// Waiting for runWhenProjectIsInitialized() ensures that the project is
// fully loaded and accessible. Otherwise, we crash. ;)
StartupManager.getInstance(updatingProject.getProject()).runWhenProjectIsInitialized(new Runnable() {
public void run() {
LOG.debug("Starting haxelib library sync...");
runUpdate();
}
});
}
/**
* Runs the update, either in the foreground or background, depending upon
* the state of the myTestInForeground debug flag.
*/
private void runUpdate() {
final ProjectTracker tracker = getUpdatingProject();
final Project project = tracker == null ? null : tracker.getProject();
if (myTestInForeground) {
doUpdateWork();
} else if (myRunInForeground) {
// TODO: Put this string in a resource bundle.
ProgressManager.getInstance().run(new Task.Modal(project, "Synchronizing with haxelib libraries...", false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
indicator.startNonCancelableSection();
doUpdateWork();
indicator.finishNonCancelableSection();
}
});
} else {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
ProgressManager.getInstance().run(
// TODO: Put this string in a resource bundle.
new Task.Backgroundable(project, "Synchronizing with haxelib libraries...", false, PerformInBackgroundOption.ALWAYS_BACKGROUND) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
doUpdateWork();
}
});
}
});
}
}
/**
* The basic bit of work that an update does.
*/
private void doUpdateWork() {
LOG.debug("Loading referenced libraries...");
ProjectTracker tracker = getUpdatingProject();
if (null == tracker) {
LOG.warn("Attempt to load libraries, but no project queued for updating.");
return;
}
synchronizeClasspaths(tracker);
finishUpdate(tracker);
}
/**
* Cleanup and queue the next in line, if any.
*
* @param up - the project that is finishing its update run.
*/
private void finishUpdate(ProjectTracker up) {
synchronized (updateSyncToken) {
LOG.assertTrue(null != updatingProject);
LOG.assertTrue(up.equals(updatingProject));
updatingProject.setUpdating(false);
updatingProject.setDirty(false);
updatingProject = null;
}
queueNextProject();
}
} // end class projectUpdateQueue
}