/*
* 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.project.structure.project.ModulePath;
import jetbrains.mps.project.structure.project.ProjectDescriptor;
import jetbrains.mps.smodel.ModuleRepositoryFacade;
import jetbrains.mps.util.annotation.ToRemove;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleListenerBase;
import org.jetbrains.mps.openapi.module.SModuleReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* MPS Project basic implementation.
* Stores a set of modules.
* Supported always by a {@link ProjectDescriptor} which stores paths to the module descriptors
* Doesn't manage lifecycle of a module descriptors other than "{@linkplain #update() update} 'em all" on demand.
* Check {@code ModuleFileChangeListener} of [mps-platform] for change tracking.
* However, tracks module renames (albeit in a bit weird way) to keep inner structures fit.
*
* @see ProjectDescriptor
*/
public abstract class ProjectBase extends Project {
private static final Logger LOG = LogManager.getLogger(ProjectBase.class);
private final ProjectManager myProjectManager = ProjectManager.getInstance();
private final Map<SModule, SModuleListenerBase> myModulesListeners = new HashMap<>();
// AP fixme must be final, however standalone mps project exposes it (a client can publicly reset the project descriptor)
protected ProjectDescriptor myProjectDescriptor;
// contract : each project module must have a corresponding ModulePath in this map
private final Map<SModule, ModulePath> myModuleToPathMap = new LinkedHashMap<>();
private final ModuleLoader myModuleLoader;
protected ProjectBase(@NotNull ProjectDescriptor projectDescriptor) {
super(projectDescriptor.getName());
myProjectDescriptor = projectDescriptor;
myModuleLoader = new ModuleLoader(this); // fixme: avoid
}
@NotNull
public String getErrors() {
return myModuleLoader.getErrors();
}
@Nullable
protected final ModulePath getPath(@NotNull SModule module) {
return myModuleToPathMap.get(module);
}
final boolean containsPath(@NotNull ModulePath modulePath) {
return myModuleToPathMap.containsValue(modulePath);
}
/**
* This is auxiliary method to update ProjectBase internal state. When a new module is added to a project,
* use {@code {@link #addModule(SModule)}}, which records the module into persistent project descriptor as well.
*
* @deprecated there is an intention to deduce virtual folders from the file system directly
*/
@ToRemove(version = 3.5)
@Deprecated
final void addModule(@NotNull ModulePath path, @NotNull SModule module) {
if (myModuleToPathMap.containsKey(module)) {
// throw new IllegalArgumentException(module + " is already in the " + this); todo enable after MPS-24400
LOG.warn(module + " is already in " + this);
return;
}
myModuleToPathMap.put(module, path);
addRenameListener(module);
}
@Override
public final void addModule(@NotNull SModule module) {
IFile descriptorFile = getDescriptorFileChecked(module);
if (descriptorFile != null) {
ModulePath path = new ModulePath(descriptorFile.getPath(), null);
addModule(path, module);
myProjectDescriptor.addModulePath(path);
myModuleLoader.fireModuleLoaded(path, module);
}
}
private void addRenameListener(@NotNull SModule module) {
if (module instanceof AbstractModule) {
// ModuleRenameListener doesn't tolerate anything but AbstractModule. Not well-mannered, imo.
ModuleRenameListener listener = new ModuleRenameListener();
myModulesListeners.put(module, listener);
module.addModuleListener(listener);
}
}
@Override
public final void removeModule(@NotNull SModule module) {
if (!myModuleToPathMap.containsKey(module)) {
LOG.warn("Module has not been registered in the project: " + module);
return;
}
final ModulePath modulePath = myModuleToPathMap.remove(module);
module.removeModuleListener(myModulesListeners.remove(module));
myModuleLoader.fireModuleRemoved(modulePath, module);
myProjectDescriptor.removeModulePath(modulePath);
}
@Nullable
private IFile getDescriptorFileChecked(SModule module) {
IFile descriptorFile = ((AbstractModule) module).getDescriptorFile();
if (descriptorFile == null) {
LOG.warn("Descriptor file path is null in the module " + module);
return null;
}
return descriptorFile;
}
@NotNull
public final List<SModule> getProjectModules() {
return new ArrayList<>(myModuleToPathMap.keySet());
}
/**
* persists the state of the project to the disk
*/
public abstract void save();
// AP: todo make final
protected void update() {
getModelAccess().runWriteAction(new Runnable() {
@Override
public void run() {
loadModules();
fireModulesLoaded();
}
});
}
/**
* AP todo : this logic must be redone alongside with filling the SLibraries with modules.
* filling libraries and projects with modules externally seems to me the best solution
*/
private void loadModules() {
getModelAccess().checkWriteAccess();
myModuleLoader.updatePathsInProject(myProjectDescriptor.getModulePaths());
}
private void fireModulesLoaded() {
getModelAccess().checkWriteAccess();
// TODO FIXME get rid of onModuleLoad
for (SModule m : getProjectModulesWithGenerators()) {
if (m instanceof AbstractModule) {
((AbstractModule) m).onModuleLoad();
}
}
}
/**
* these are our own project opened/closed events.
* in the case of idea platform presence they are triggered from the corresponding idea project opened/closed events.
* in the other case they are triggered at the init/dispose methods
*/
public void projectOpened() {
LOG.info("Project '" + getName() + "' is opened");
myProjectManager.projectOpened(this);
}
public void projectClosed() {
checkNotDisposed();
LOG.info("Project '" + getName() + "' is closing");
myProjectManager.projectClosed(this);
getModelAccess().runWriteAction(() -> new ModuleRepositoryFacade(ProjectBase.this).unregisterModules(ProjectBase.this));
getProjectModules().forEach(this::removeModule);
}
@Override
public boolean isOpened() {
return ProjectManager.getInstance().getOpenedProjects().contains(this);
}
@NotNull
public String toString() {
return "MPS Project [" + getName() + (isDisposed() ? ", disposed]" : "]");
}
/**
* calls {@link ProjectDataSource#loadDescriptor()} and set the new project descriptor
* makes sense to use this method with the {@link #update()} together
* to avoid the inconsistency between the project modules and the descriptor state.
*/
protected final void loadDescriptor(@NotNull ProjectDataSource dataSource) {
checkNotDisposed();
myProjectDescriptor = dataSource.loadDescriptor();
}
// Used to live in StandaloneMPSProject. I don't see why it's restricted to that one, provided any
// ProjectBase derivative knows aboud ModulePath and its virtual folder.
protected void setVirtualFolder(@NotNull SModule module, String newFolder) {
// TODO: remove duplication of ModulePath in ProjectBase.myModuleToPathMap to avoid handling both lists
ModulePath modulePath = getPath(module);
if (modulePath != null) {
ModulePath newPath = modulePath.withVirtualFolder(newFolder);
myProjectDescriptor.replacePath(modulePath, newPath);
myModuleToPathMap.put(module, newPath);
} else {
LOG.warn("Could not set virtual folder for the module " + module + ", module could not be found");
}
}
public final void addListener(@NotNull ProjectModuleLoadingListener listener) {
myModuleLoader.addListener(listener);
}
public final void removeListener(@NotNull ProjectModuleLoadingListener listener) {
myModuleLoader.removeListener(listener);
}
// XXX use of SModule listener to detect renames smells wrong. I'd say Project shall deal with files, on a lower level than SRepository.
// Perhaps, this comes along missing file rename event from FileListener?
private class ModuleRenameListener extends SModuleListenerBase {
@Override
public void moduleRenamed(@NotNull SModule module, @NotNull SModuleReference oldRef) {
// why exceptions, why so intolerable? Just because we added the listener to a module with file?
if (!(module instanceof AbstractModule)) {
throw new IllegalArgumentException("Support only abstract module here " + module);
}
ModulePath oldPath = myModuleToPathMap.remove(module);
IFile descriptorFile = ((AbstractModule) module).getDescriptorFile();
if (descriptorFile == null) {
throw new IllegalArgumentException("The descriptor file is null " + module);
}
ModulePath newPath = new ModulePath(descriptorFile.getPath(), oldPath.getVirtualFolder());
myProjectDescriptor.replacePath(oldPath, newPath);
myModuleToPathMap.put(module, newPath);
}
}
}