/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.core; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.DependencyScope; import com.intellij.openapi.roots.LibraryOrderEntry; import com.intellij.openapi.roots.ModuleOrderEntry; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.ex.ProjectRootManagerEx; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import gw.config.CommonServices; import gw.config.IExtensionFolderLocator; import gw.config.IPlatformHelper; import gw.fs.IDirectory; import gw.lang.GosuShop; import gw.lang.init.GosuInitialization; import gw.lang.parser.IGosuParser; import gw.lang.reflect.IType; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.java.IJavaType; import gw.lang.reflect.java.JavaTypes; import gw.lang.reflect.module.Dependency; import gw.lang.reflect.module.IExecutionEnvironment; import gw.lang.reflect.module.IFileSystem; import gw.lang.reflect.module.IJreModule; import gw.lang.reflect.module.IModule; import gw.plugin.ij.filesystem.IDEAFileSystem; import gw.plugin.ij.sdk.GosuSdkType; import gw.plugin.ij.util.GosuModuleUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static com.google.common.collect.Iterables.filter; public class TypeSystemStarter { private static final Logger LOG = Logger.getInstance(TypeSystemStarter.class); public static final String JAR_INDICATOR = ".jar!"; private static final Map<Project, TypeSystemStarter> INSTANCES = new WeakHashMap<>(); public enum StartupStatus {NOT_STARTED, STARTING, STARTED, STOPPING} private final AtomicReference<StartupStatus> _status = new AtomicReference<>(StartupStatus.NOT_STARTED); private final AtomicInteger _concurrentRefresh = new AtomicInteger(0); private final Project _project; @Nullable Module[] _allIJModules; public static TypeSystemStarter instance(Project project) { TypeSystemStarter typeSystemStarter = INSTANCES.get(project); if (typeSystemStarter == null) { INSTANCES.put(project, typeSystemStarter = new TypeSystemStarter(project)); } return typeSystemStarter; } public static TypeSystemStarter instance(@NotNull IType type) { return instance(projectFrom(type)); } public static TypeSystemStarter instance(@NotNull IModule module) { return instance(projectFrom(module)); } public static TypeSystemStarter instance(@NotNull IExecutionEnvironment execEnv) { return instance(projectFrom(execEnv)); } @NotNull public static Project projectFrom(@NotNull IType type) { return (Project) type.getTypeLoader().getModule().getExecutionEnvironment().getProject().getNativeProject(); } @NotNull public static Project projectFrom(@NotNull IModule module) { return (Project) module.getExecutionEnvironment().getProject().getNativeProject(); } @NotNull public static Project projectFrom(@NotNull IExecutionEnvironment execEnv) { return (Project) execEnv.getProject().getNativeProject(); } public TypeSystemStarter(Project project) { _project = project; } public void start(@NotNull Project project) { if (_status.compareAndSet(StartupStatus.NOT_STARTED, StartupStatus.STARTING)) { try { ModuleManager moduleManager = ModuleManager.getInstance(project); _allIJModules = moduleManager.getModules(); initializeGosu(project); _status.set(StartupStatus.STARTED); } catch (RuntimeException e) { _status.set(StartupStatus.NOT_STARTED); throw e; } catch (Error e) { _status.set(StartupStatus.NOT_STARTED); throw e; } } else { // XXX type system already started -- concurrent start?! ack! throw new IllegalStateException("ACK! Attempted concurrent start of type system."); } } /** * Caller must verify type system is in started status after this call returns to safely continue. */ public void startRefresh() { _concurrentRefresh.incrementAndGet(); } public void stopRefresh() { if (_concurrentRefresh.decrementAndGet() == 0) { synchronized (_concurrentRefresh) { _concurrentRefresh.notifyAll(); } } } void initializeGosu(@NotNull Project project) { String circularDependency = GosuModuleUtil.getCircularModuleDependency(project); if (circularDependency != null) { throw new GosuPluginException("Gosu does not support circular dependencies.\n" + circularDependency, PluginFailureReason.CIRCULAR_DEPENDENCY); } // make sure IJavaType is initialized, because if another thread tries // to initialize it without the typesystem lock, we'll deadlock //noinspection UnusedDeclaration Class c = IJavaType.class; //noinspection UnusedAssignment c = IGosuParser.class; IExecutionEnvironment execEnv = TypeSystem.getExecutionEnvironment(PluginLoaderUtil.getFrom(project)); GosuInitialization.instance(execEnv).uninitializeRuntime(); try { CommonServices.getKernel().redefineService_Privileged(IFileSystem.class, new IDEAFileSystem()); } catch (Exception e) { LOG.error(e); } CommonServices.getKernel().redefineService_Privileged(IExtensionFolderLocator.class, new IDEAExtensionFolderLocator()); CommonServices.getKernel().redefineService_Privileged(IPlatformHelper.class, new IDEAPlatformHelper(project)); List<IModule> modules = defineModules(project); GosuInitialization.instance(execEnv).initializeMultipleModules(modules); IModule module = execEnv.getModule(IExecutionEnvironment.GLOBAL_MODULE_NAME); TypeSystem.pushModule(module); try { Object o1 = IGosuParser.NaN; Object o2 = JavaTypes.DOUBLE(); } finally { TypeSystem.popModule(module); } } @NotNull private List<IModule> defineModules(@NotNull Project project) { IExecutionEnvironment execEnv = TypeSystem.getExecutionEnvironment(PluginLoaderUtil.getFrom(project)); execEnv.createJreModule( ); final List<IDirectory> allSourcePaths = Lists.newArrayList(); final List<IDirectory> allRoots = Lists.newArrayList(); final Map<Module, IModule> modules = Maps.newHashMap(); final List<IModule> allModules = Lists.newArrayList(); for (Module ijModule : _allIJModules) { final IModule module = defineModule(ijModule); if (module != null) { allSourcePaths.addAll(module.getSourcePath()); allRoots.addAll(module.getRoots()); modules.put(ijModule, module); allModules.add(module); } } for (Module ijModule : _allIJModules) { final IModule module = modules.get(ijModule); for (Module child : ModuleRootManager.getInstance(ijModule).getDependencies()) { IModule moduleDep = modules.get(child); if (moduleDep != null) { module.addDependency(new Dependency(moduleDep, isExported(ijModule, child))); } } } addImplicitJreModuleDependency(project, allModules); allSourcePaths.addAll(execEnv.getJreModule().getSourcePath()); IModule _globalModule = GosuShop.createGlobalModule(execEnv); _globalModule.configurePaths(Collections.<IDirectory>emptyList(), allSourcePaths); List<IModule> rootModules = findRootModules(allModules); for (IModule rootModule : rootModules) { _globalModule.addDependency(new Dependency(rootModule, true)); } // _globalModule.addDependency(new Dependency(execEnv.getJreModule(), true)); _globalModule.setRoots(allRoots); allModules.add(_globalModule); return allModules; } public List<IModule> findRootModules(List<IModule> modules) { List<IModule> moduleRoots = new ArrayList<>(modules); for (IModule module : modules) { for (Dependency d : module.getDependencies()) { moduleRoots.remove(d.getModule()); } } return moduleRoots; } public static boolean isExported(@NotNull Module ijModule, Module child) { for (OrderEntry entry : ModuleRootManager.getInstance(ijModule).getOrderEntries()) { if (entry instanceof ModuleOrderEntry) { final ModuleOrderEntry moduleEntry = (ModuleOrderEntry) entry; final DependencyScope scope = moduleEntry.getScope(); if (!scope.isForProductionCompile() && !scope.isForProductionRuntime()) { continue; } final Module module = moduleEntry.getModule(); if (module != null && module == child) { return moduleEntry.isExported(); } } } return false; } private void addImplicitJreModuleDependency(Project project, @NotNull List<IModule> modules) { IJreModule jreModule = GosuModuleUtil.getJreModule(project); updateJreModuleWithProjectSdk(project, jreModule); for (IModule module : modules) { module.addDependency(new Dependency(jreModule, true)); } modules.add(jreModule); } public static void updateJreModuleWithProjectSdk(Project project, @NotNull IJreModule jreModule) { //note: A module can declare its own SDK, separate from the project's SDK. If we ever handle // this case we'll create a separate ExecutionEnvironment rooted at the module. The idea // is that a JRE represents a runtime environment and the SDK essentially defines the JRE. final ProjectRootManager rootManager = ProjectRootManagerEx.getInstance(project); Sdk projectSdk = rootManager.getProjectSdk(); if (projectSdk != null) { final VirtualFile[] classFiles = projectSdk.getRootProvider().getFiles(OrderRootType.CLASSES); if (classFiles.length > 0) { jreModule.configurePaths(toDirectories(classFiles), Collections.<IDirectory>emptyList()); } else { throw new GosuPluginException("Project SDK does not have any files at this location:\n" + projectSdk.getHomePath() + "\nPlease, fix your current SDK or switch to another one.", PluginFailureReason.INVALID_SDK); } jreModule.setNativeSDK(projectSdk); } else { throw new GosuPluginException("Project SDK not defined.", PluginFailureReason.NO_SDK); } } private static List<IDirectory> toDirectories(@NotNull VirtualFile[] files) { final List<IDirectory> result = Lists.newArrayList(); for (VirtualFile f : files) { File file = new File(stripExtraCharacters(f.getPath())); IDirectory dir = CommonServices.getFileSystem().getIDirectory(file); result.add(dir); } return result; } @NotNull public List<Module> getAllRequiredModules(@NotNull Module p) { Set<Module> visitedProjects = new HashSet<>(); List<Module> projects = new ArrayList<>(); getAllRequiredProjects(p, projects, visitedProjects); return projects; } private void getAllRequiredProjects(@NotNull Module ijModule, @NotNull List<Module> ijModuleList, @NotNull Set<Module> visitedModules) { visitedModules.add(ijModule); for (Module otherModule : ModuleRootManager.getInstance(ijModule).getDependencies()) { if (!visitedModules.contains(otherModule)) { ijModuleList.add(otherModule); getAllRequiredProjects(otherModule, ijModuleList, visitedModules); } } } public IModule defineModule(@NotNull Module ijModule) { IModule gosuModule = GosuShop.createModule(TypeSystem.getExecutionEnvironment(PluginLoaderUtil.getFrom(ijModule.getProject())), ijModule.getName()); // File moduleLocation = new File(ijModule.getProject().getLocation()); // IDirectory eclipseModuleRoot = CommonServices.getFileSystem().getIDirectory(moduleLocation); // gosuModule.addRoot(eclipseModuleRoot); List<VirtualFile> sourceFolders = getSourceRoots(ijModule); gosuModule.configurePaths(getClassPaths(ijModule), Lists.transform(sourceFolders, ToDirectory.INSTANCE)); VirtualFile root = sourceFolders.size() == 1 ? sourceFolders.get(0).getParent() : VfsUtil.getCommonAncestor(sourceFolders); if (root != null) { IDirectory sourceRoot = CommonServices.getFileSystem().getIDirectory(new File(root.getPath())); gosuModule.setRoots(Collections.<IDirectory>singletonList(sourceRoot)); } //Fix this // ModuleRootManager rootManager = ModuleRootManager.getInstance(ijModule); // File outputPath = rootManager.getOutputLocation().removeFirstSegments(1).toFile(); // outputPath = new File(moduleLocation, outputPath.getPath()); // gosuModule.setOutputPath(CommonServices.getFileSystem().getIDirectory(outputPath)); gosuModule.setNativeModule(new IJNativeModule(ijModule)); return gosuModule; } public static List<IDirectory> getSourceFolders(@NotNull Module ijModule) { return Lists.transform(getSourceRoots(ijModule), ToDirectory.INSTANCE); } public static List<VirtualFile> getSourceRoots(@NotNull Module ijModule) { final ModuleRootManager moduleManager = ModuleRootManager.getInstance(ijModule); final List<VirtualFile> sourcePaths = Lists.newArrayList(); List<VirtualFile> excludeRoots = Arrays.asList(moduleManager.getExcludeRoots()); for (VirtualFile sourceRoot : moduleManager.getSourceRoots()) { if (!excludeRoots.contains(sourceRoot)) { sourcePaths.add(sourceRoot); } } return sourcePaths; } private enum ToDirectory implements Function<VirtualFile, IDirectory> { INSTANCE; @Override public IDirectory apply(VirtualFile file) { String sourcePath = file.getPath(); if (sourcePath.contains(JAR_INDICATOR)) { sourcePath = sourcePath.substring(0, sourcePath.length() - 2); } return CommonServices.getFileSystem().getIDirectory(new File(sourcePath)); } } public void stop(@NotNull Project project) { if (_status.compareAndSet(StartupStatus.STARTED, StartupStatus.STOPPING)) { try { // wait for concurrent refreshes to complete... synchronized (_concurrentRefresh) { try { if (_concurrentRefresh.get() > 0) { _concurrentRefresh.wait(10000); } } catch (InterruptedException ex) { // Timeout for safety. Not sure if this could happen... seems doubtful. } } GosuInitialization.instance(TypeSystem.getExecutionEnvironment(PluginLoaderUtil.getFrom(project))).uninitializeMultipleModules(); _allIJModules = null; } finally { // XXX could fail to shutdown, so maybe put into failed state in such cases? _status.set(StartupStatus.NOT_STARTED); } } else { // XXX type system not started yet, cannot stop. Implement interrupt capability? throw new IllegalStateException("ACK! Attempted to stop type system while it was still starting... Do we have to support this?"); } } public boolean isStarted() { return _status.get() == StartupStatus.STARTED; } public static List<IDirectory> getClassPaths(@NotNull Module ijModule) { List<String> paths = getDirectClassPaths(ijModule); for (Iterator<String> it = paths.iterator(); it.hasNext();) { String url = it.next(); if (dependencyChainContains(ijModule, url, new ArrayList<Module>())) { it.remove(); } } List<IDirectory> dirs = new ArrayList<>(); for (String path : paths) { dirs.add(CommonServices.getFileSystem().getIDirectory(new File(path))); } return dirs; } private static enum GosuCoreJar implements Predicate<File> { INSTANCE; @Override public boolean apply(@Nullable File file) { // FIXME: Stupid way to detect if JAR is Gosu JAR. return file.getName().startsWith("gosu-core-"); } } private static List<String> getDirectClassPaths(Module ijModule) { final ModuleRootManager rootManager = ModuleRootManager.getInstance(ijModule); Sdk sdk = rootManager.getSdk(); boolean gosuSdk = sdk != null && sdk.getSdkType() == GosuSdkType.getInstance(); final List<OrderEntry> orderEntries = Arrays.asList(rootManager.getOrderEntries()); Predicate<File> ignoredLibs = gosuSdk ? GosuCoreJar.INSTANCE : Predicates.<File>alwaysFalse(); List<String> paths = new ArrayList<>(); for (LibraryOrderEntry entry : filter(orderEntries, LibraryOrderEntry.class)) { final Library lib = entry.getLibrary(); if (lib != null) { for (VirtualFile virtualFile : lib.getFiles(OrderRootType.CLASSES)) { final File file = new File(stripExtraCharacters(virtualFile.getPath())); if (file.exists() && !ignoredLibs.apply(file)) { paths.add(file.getAbsolutePath()); } } } } return paths; } private static boolean dependencyChainContains(Module ijModule, String path, List<Module> visited) { if (!visited.contains(ijModule)) { visited.add(ijModule); ModuleRootManager rootManager = ModuleRootManager.getInstance(ijModule); for (Module dep : rootManager.getDependencies()) { if (getDirectClassPaths(dep).contains(path) || dependencyChainContains(dep, path, visited)) { return true; } } } return false; } @NotNull private static String stripExtraCharacters(@NotNull String fileName) { //TODO-dp this is not robust enough (eliminated !/ at the end of the jar) if (fileName.endsWith("!/")) { fileName = fileName.substring(0, fileName.length() - 2); } return fileName; } }