/* * Copyright 2003-2017 JetBrains s.r.o. * * 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 jetbrains.mps.project; import jetbrains.mps.library.ModulesMiner; import jetbrains.mps.library.ModulesMiner.ModuleHandle; import jetbrains.mps.project.structure.modules.GeneratorDescriptor; import jetbrains.mps.project.structure.project.ModulePath; import jetbrains.mps.smodel.ModuleRepositoryFacade; import jetbrains.mps.util.Pair; import jetbrains.mps.vfs.FileSystems; import jetbrains.mps.vfs.IFile; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.module.SModule; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; /** * Extracted project modules loading logic. Currently used in the project only. * Supposed to be merged with the SLibraries modules loading logic (it is essentially the same thing) * TODO the code structure is a shame, rewrite * * Created by apyshkin on 11/5/15. */ final class ModuleLoader { private static final Logger LOG = LogManager.getLogger(ModuleLoader.class); @NotNull private final ProjectBase myProject; private final List<ProjectModuleLoadingListener> myListeners = new CopyOnWriteArrayList<ProjectModuleLoadingListener>(); private final StringBuilder myErrors = new StringBuilder(); public ModuleLoader(@NotNull ProjectBase project) { myProject = project; } private Collection<Pair<ModulePath, SModule>> getRemovedModules(List<ModulePath> newModulePaths) { ArrayList<Pair<ModulePath, SModule>> removedModules = new ArrayList<>(); for (SModule oldModule : myProject.getProjectModules()) { ModulePath oldModulePath = myProject.getPath(oldModule); if (!newModulePaths.contains(oldModulePath)) { removedModules.add(new Pair<>(oldModulePath, oldModule)); } } return removedModules; } private List<ModulePath> getPathsToLoad(List<ModulePath> newModulePaths) { List<ModulePath> pathsToLoad = new ArrayList<>(); for (ModulePath newModulePath : newModulePaths) { if (!myProject.containsPath(newModulePath)) { pathsToLoad.add(newModulePath); } } return pathsToLoad; } @NotNull public String getErrors() { return myErrors.toString(); } /** * updates module paths in the project. */ void updatePathsInProject(final List<ModulePath> newModulePaths) { LOG.info("Loading modules..."); clearErrorsBuffer(); // Note the order which matters (the case is when the modules.xml is updated from the FS directly -- // one of the modules might change its virtual folder but not the location // in this case we need to remove that module from project and insert it again final Collection<Pair<ModulePath, SModule>> removedModules = getRemovedModules(newModulePaths); removeAbsentModules(removedModules); final List<ModulePath> pathsToLoad = getPathsToLoad(newModulePaths); int loadedModules = loadNewPaths(pathsToLoad); LOG.info(String.format("Modules are loaded: %d new; %d removed", loadedModules, removedModules.size())); } /** * @return the number of successfully loaded modules */ private int loadNewPaths(final List<ModulePath> pathsToLoad) { final ModulesMiner modulesMiner = new ModulesMiner(); final Map<ModuleHandle, ModulePath> handleToPath = new HashMap<>(); for (ModulePath modulePath : pathsToLoad) { String descriptorPath = modulePath.getPath(); IFile descriptorFile = FileSystems.getDefault().getFile(descriptorPath); if (descriptorFile.exists()) { ModuleHandle handle = modulesMiner.loadModuleHandle(descriptorFile); handleToPath.put(handle, modulePath); } else { // TODO listen to file location in the MPSProject // AP : it is kind of strange having module watching for removing/changing its file descriptor and having someone else // watching for the module creation. I believe everything which concerns the module file system watching must be done in one place. error(String.format("Can't load module from %s. File doesn't exist.", descriptorPath)); fireModuleNotFound(modulePath); } } int loadedModules = 0; ModuleRepositoryFacade repoFacade = new ModuleRepositoryFacade(myProject); ArrayList<ModuleHandle> postponedGeneratorHandles = new ArrayList<>(pathsToLoad.size()); // XXX This code resembles ProjectModulesFiller (ProjectStrategyBase). Do we need to keep them separate? for (ModuleHandle handle : modulesMiner.getCollectedModules()) { if (handle.getDescriptor() instanceof GeneratorDescriptor) { postponedGeneratorHandles.add(handle); // FIXME getCollectedModules() yields extended set of ModuleHandle compared to collection of MM.loadModuleHandle return values // that are keys in handleToPath. Namely, there are distinct mined handles for generator modules, which Project implementation // at the moment doesn't expect to see. Perhaps, handleToPath shall map descriptorFile -> ModulePath instead? continue; } ModulePath modulePath = handleToPath.get(handle); if (handle.getDescriptor() != null) { SModule module = repoFacade.instantiateModule(handle, myProject); // it's quite tempting, indeed, to move project update (i.e. addModule) into listener ProjectModuleLoadingListener.moduleLoaded // just need to sort out ModuleLoader and Project relationship. myProject.addModule(modulePath, module); ++loadedModules; // XXX Here, in ProjectModuleLoadingListener/ModuleFileChangeListener, we track language files only, and rely on regular // Language.reloadAfterDescriptorChange code to reflect changes in Generator modules fireModuleLoaded(modulePath, module); } else { error(String.format("Can't load module from %s. Unknown file type.", handle.getFile().getPath())); fireModuleTypeIsUnknown(modulePath); } } // at the moment, MRF is not capable to register a generator sooner that its language. To make sure no generator comes first, // we postpone instantiation of all of them. for (ModuleHandle generatorHandle : postponedGeneratorHandles) { repoFacade.instantiateModule(generatorHandle, myProject); // neither myProject.addModule() nor ++loadedModules, nor fireModuleLoaded were considered here as existing code never did it for generator modules // XXX This, however, likely to change once generators become standalone modules. } return loadedModules; } private void removeAbsentModules(final Collection<Pair<ModulePath, SModule>> removedModules) { for (Pair<ModulePath, SModule> p : removedModules) { fireModuleRemoved(p.o1, p.o2); myProject.removeModule(p.o2); new ModuleRepositoryFacade(myProject).unregisterModule(p.o2); } } private void clearErrorsBuffer() { myErrors.setLength(0); } private void error(@NotNull String text) { if (myErrors.length() > 0) { myErrors.append(System.getProperty("line.separator")); } myErrors.append(text); LOG.error(text); } public void addListener(@NotNull ProjectModuleLoadingListener listener) { myListeners.add(listener); } public void removeListener(@NotNull ProjectModuleLoadingListener listener) { if (!myListeners.contains(listener)) { LOG.warn("Listener could not be found : " + listener); } myListeners.remove(listener); } private void fireModuleNotFound(ModulePath modulePath) { for (ProjectModuleLoadingListener listener : myListeners) { listener.moduleNotFound(modulePath); } } private void fireModuleTypeIsUnknown(ModulePath modulePath) { for (ProjectModuleLoadingListener listener : myListeners) { listener.moduleTypeIsUnknown(modulePath); } } /*package*/ void fireModuleRemoved(ModulePath modulePath, SModule module) { for (ProjectModuleLoadingListener listener : myListeners) { listener.moduleRemoved(modulePath, module); } } /*package*/ void fireModuleLoaded(ModulePath modulePath, SModule module) { for (ProjectModuleLoadingListener listener : myListeners) { listener.moduleLoaded(modulePath, module); } } }